felixeckert 0 Posted December 6, 2022 Hey, Im currently working on a Program in Delphi and have a "uLogAbstractor" unit tot handle logging across threads, this unit essentially just writes the log message and level to a global variable and the sends a Windows Message using the Winapi to the Main Form which has the logging components. Now I have noticed that regardless of the thread, the global variable containing the message seems to reset to its previous value after ive called PostMessage. Weirdly this doesn't occur if i output it using ShowMessage before calling PostMessage. Here is the log abstractor unit uLogAbstractor; interface uses SysUtils, Winapi.Windows, uConsts, Vcl.Dialogs; procedure Log(const lvl: Integer; const msg: String); procedure LogF(const lvl: Integer; const msg: String; const _format: array of const); var msghandle: Cardinal = 0; logmsg: String; loglvl: Integer; implementation procedure Log(const lvl: Integer; const msg: String); begin if msghandle = 0 then exit; loglvl := lvl; logmsg := msg; // ShowMessage(logmsg); PostMessage(msghandle, WM_LOG_MESSAGE, 0, 0); end; procedure LogF(const lvl: Integer; const msg: String; const _format: array of const); begin if msghandle = 0 then exit; loglvl := lvl; logmsg := Format(msg, _format); PostMessage(msghandle, WM_LOG_MESSAGE, 0, 0); end; end. And here is the procedure handling the message in the main form procedure TfrmMain.WMLogMessage(var msg: string); begin FLogger.Log(uLogAbstractor.loglvl, uLogAbstractor.logmsg); if uLogAbstractor.loglvl = LOG_LVL_DBG then begin if FLogger.FLogDebug then memoLog.Lines.Add(FLogger.FLastMsg); end else memoLog.Lines.Add(FLogger.FLastMsg); end; Does anyone know the reason for this behaviour? I'd be grateful for any help! Share this post Link to post
programmerdelphi2k 237 Posted December 6, 2022 (edited) First, the use of global variables/objects does not guarantee that their value is the same as the one sent by the sender, as many senders may change the expected value, especially in an asynchronous (or not) sending/receiving. Here, you will certainly have a conflict of interest between the various calls involved. To try to guarantee receipt (expected), you would have to use some kind of lock to the variable/object with the expected value. Creating a kind of "access queue" to it! However, this still does not guarantee receipt in the order sent, especially if the sending is asynchronous. Perhaps, if you use a way to "enumerate" the sent messages, for example, in a pair: ("index", "message"). Then, on the other side, you could sort in order of arrival, and thus have your queue sorted! But if order of arrival doesn't matter, forget about this part! See about the use of means of blocking simultaneous access to information, such as: TCriticalSection, TMonitor, Semaphores, etc... When using "ShowMessage()" everything seems to work, precisely because it "stops" the process until the user clicks "OK" to continue! It's just a false positive! or a positive-false! Edited December 6, 2022 by programmerdelphi2k Share this post Link to post
programmerdelphi2k 237 Posted December 6, 2022 maybe, "Observer Pattern" (or similar) can replace your "PostMessage", and allow add more params with help you, for example using "pair" values! Form1 subscribe a list (global but protected on access), and the threads notify this list! The "list", can be a object TList or similar, to register all "subscribers" (for example, Form1). Then, the threads look this list and send the notifications for any one interested on the message! basically, the same behaviour than "PostMessage" do it! not necessary!!! Share this post Link to post
felixeckert 0 Posted December 6, 2022 Thanks for the reply! An Object Queue where I would enqueue Records holding the message data would work fine? Im not too experienced with threading Share this post Link to post
programmerdelphi2k 237 Posted December 6, 2022 (edited) For sure, I dont know! Because each case is a case! It would be +/- : uses System.Generics.Collections; type TMyObjList = TList<string>; // example using "strings", but can be any others types, including other objects // TMyObjList = TList<MyType>; var MyObjList: TMyObjList; ... // to start jobs, you would create a list MyObjList := TMyObjList.Create; // .... // // accessing items you would have protect it before any read/write to avoid conflicts // using TMonitor, TCriticalSection, etc... // xxxx.Lock( MyObjList ); // MyObjList.Add('message...'); MyObjList.Items[ 0 ]; MyObjList.Remove('message...'); MyObjList[ 0 ] := 'message...'; // after read/write you release the object xxxx.UnLock( MyObjList ); //.... // // at end "all" (for example, end app) you can destroy the list MyObjList.Free; better would be have yourself "class" to easy acess to your list Edited December 6, 2022 by programmerdelphi2k Share this post Link to post
FPiette 383 Posted December 6, 2022 1 hour ago, felixeckert said: An Object Queue where I would enqueue Records holding the message data would work fine? Only if you use a critical section to protect the queues from multiple access by the main thread and the worker thread. Use a custom message to signal the main thread that a message has been queued. Another approach is to use only Windows messaging. The sender will LParam to store what you name Lvl and use LParam to store a pointer to an memory block that you allocate once per message and where you copy the message, including a nul character to give end of message. The message handler MUST free that memory block after getting the message out of it. Share this post Link to post
aehimself 396 Posted December 11, 2022 I'm always using a "subscriber model" when it comes to logging. I have a singleton class, with 2 important methods: Subscribe and Log. You can subscribe with a TLogProc = Procedure(Const inLineToLog: String) Of Object; for example, which is only adding the method pointer to a TArray / TList. Logging uses a critical section and loops through all subscribed loggers, like: Procedure TMyLogger.Log(const inLineToLog: String); Var logrefproc: TLogProc; s: String; Begin TMonitor.Enter(Self); Try s := '[' + DateTimeToStr(Now)+ '] ' + inLineToLog.Trim; For logrefproc In _logrefprocs Do Try logrefproc(s); Except // Swallow logger exceptions. End; Finally TMonitor.Exit(Self); End; End; This way you can call MyLogger.Log from any thread and the message will be distributed to all subscribed loggers. This way, printing to the console window, appending to a TMemo, saving it to a local file and pusing it up to the cloud can all happen - if you wish. As you already seem having some kind of a logger object, moving it to a separate unit and using this logic should suit your needs well. Just make sure the logging methods aren't taking long AND are synchronized, as they will be called by the thread's context! When you have lots of entries I found it easier to add them to a TList<String> object and using a TTimer to dump the contents and empty the list every second or so: Procedure TLogFrame.InternalLog(const inLogWhat: String); Begin System.TMonitor.Enter(_linestolog); Try _linestolog.Append(inLogWhat + sLineBreak); Finally System.TMonitor.Exit(_linestolog); End; End; Procedure TLogFrame.LogTimerTimer(Sender: TObject); Begin LogTimer.Enabled := False; Try System.TMonitor.Enter(_linestolog); Try If _linestolog.Length > 0 Then Begin LogMemo.Lines.BeginUpdate; Try LogMemo.Text := LogMemo.Text + _linestolog.ToString; CursorToEnd; _linestolog.Clear; Finally LogMemo.Lines.EndUpdate; End; End; Finally System.TMonitor.Exit(_linestolog); End; Finally LogTimer.Enabled := True; End; End; And my OCD forces me to tell you, that On 12/6/2022 at 2:28 PM, felixeckert said: if uLogAbstractor.loglvl = LOG_LVL_DBG then begin if FLogger.FLogDebug then memoLog.Lines.Add(FLogger.FLastMsg); end else memoLog.Lines.Add(FLogger.FLastMsg); can be simplified to: if (uLogAbstractor.loglvl <> LOG_LVL_DBG) Or FLogger.FLogDebug then memoLog.Lines.Add(FLogger.FLastMsg); Share this post Link to post