2

I have a form with a TMemo that I want to show what is going on in several services started by the application.

What I have running:

  • idHTTPServer running with idContext responding to requests
  • a Thread downloading updates from Dropbox
  • idUDPServer responding to UDP requests
  • another thread taking care of some database stuff.
  • the main application thread also needed to add log

Basically, I need to know how to create a standard, unified, thread safe way to channel the log messages to my TMemo and keep the user updated of what is going on.

Ken White
  • 123,280
  • 14
  • 225
  • 444
Eduardo Elias
  • 1,742
  • 1
  • 22
  • 49

2 Answers2

7

Since you are already using Indy anyway, you can use Indy's TIdSync (synchronous) or TIdNotify (asynchronous) class to access the TMemo safely. For simple logging purposes, I would use TIdNotify, eg:

type
  TLog = class(TIdNotify)
  protected
    FMsg: string;
    procedure DoNotify; override;
  public
    class procedure LogMsg(const AMsg; string);
  end;

procedure TLog.DoNotify;
begin
  Form1.Memo1.Lines.Add(FMsg);
end;

class procedure TLog.LogMsg(const AMsg: string);
begin
  with TLog.Create do
  try
    FMsg := AMsg;
    Notify;
  except
    Free;
    raise;
  end;
end;

Then you can directly call it in any thread like this:

TLog.LogMsg('some text message here');

UPDATE: in Delphi 2009 and later, you can use anonymous procedures with the static versions of TThread.Synchronize() and TThread.Queue(), thus making Indy's TIdSync and TIdNotify classes obsolete, eg:

type
  TLog = class
  public
    class procedure LogMsg(const AMsg; string);
  end;

class procedure TLog.LogMsg(const AMsg: string);
begin
  TThread.Queue(nil,
    procedure
    begin
      Form1.Memo1.Lines.Add(AMsg);
    end
  );
end;
Remy Lebeau
  • 555,201
  • 31
  • 458
  • 770
  • OK I have implemented and worked just fine. Just to make sure, I have this class and all others in units out of the main file. Is it needed to have any reference on the main file to any Indy unit? I have added IdSync, however I am not sure. – Eduardo Elias Jul 08 '13 at 22:22
  • Well, since `TLog.DoNotify()` accesses `Form1`, the `TLog` unit it would need a reference to `Form1`'s unit. – Remy Lebeau Jul 09 '13 at 00:06
4

Basically, you can build a thread that receive all the message (here, it is a function AddEvent). Messages are queued (and timestamped) and written down to the memo when possible (if you're under heavy load...).

Don't forget to clean the memo if it exceeds a number of line, add exception handling etc...

I use something like this :

    TThreadedMsgEvent = class( TThread )
    private
          FLock : TCriticalSection;
          FStr : TQueue<String>;
          FMemo : TMemo;
          function GetEvent : String;
    protected
          procedure Execute; override;
    public
          procedure AddEvent( aMsg : String );

          constructor Create( AMemo: TMemo );
          destructor Destroy; override;
    end;
implementation

{ TThreadedMsgEvent }

procedure TThreadedMsgEvent.AddEvent(aMsg: String);
begin
     FLock.Acquire;
     FStr.Enqueue( FormatDateTime('DD/MM/YY HH:NN:SS.ZZZ',Now)+ ' : '+ aMsg );
     FLock.Release;
end;

constructor TThreadedMsgEvent.Create(aMemo: TMemo);
begin
  inherited Create(True);

  FreeOnTerminate := False;
  FOnMessage := ACallBack;
  FStr := TQueue<String>.Create();
  FLock      := TCriticalSection.Create;
  FMemo := aMemo;
  Resume;
end;

destructor  TThreadedMsgEvent.Destroy; override;
begin
      FreeAndNil( FStr );
      FreeAndNil( FLock );
end;

procedure TThreadedMsgEvent.Execute;
begin
  while not Terminated do
  begin

      try
         if (FStr.Count > 0) then
         begin
              if Assigned( aMemo ) then
              begin
                    TThread.synchronize( procedure
                                         begin
                                            FMemo.Lines.Add( GetEvent );    
                                         end; );
              end;

         end;
      except
      end;
      TThread.Sleep(1);
  end;

end;

function TThreadedMsgEvent.GetEvent: String;
begin
     FLock.Acquire;
     result := FStr.Dequeue;
     FLock.Release;
end;

You can also notify this thread with Windows Messages. It might be easier as you won't need any reference to this thread in your classes.

RobertFrank
  • 7,332
  • 11
  • 53
  • 99
Greg M.
  • 229
  • 1
  • 9
  • 3
    You should use a blocking queue. That needs an event to signal that items are available. There are many good examples and classes around. That pseudo busy loop is not good. – David Heffernan Jul 06 '13 at 21:31
  • @DavidHeffernan Could you point to a good example of this blocking queue? – Eduardo Elias Jul 06 '13 at 21:38
  • is TThreadedQueue capable of doing the same thing? if all the thread reference the same TThreadedQueue object to add string and in the main Thread to consume the strings. Is it safe? Is it bug free? – Eduardo Elias Jul 06 '13 at 22:11
  • @eelias, `TThreadedQueue` can be used if you have Delphi-XE2 update 4 or later. See [`TThreadedQueue not capable of multiple consumers?`](http://stackoverflow.com/questions/4856306/tthreadedqueue-not-capable-of-multiple-consumers) for more information. – LU RD Jul 07 '13 at 06:20
  • 4
    Using a thread for this purpose is not only superfluous but does in this case needlessly reduce performance. First by using the busy loop David refers to, second by causing lots of additional and unnecessary thread context switches. Get rid of the thread, create a simple thread-safe log queue, and have it post a single message to the GUI thread when the first item is added to the queue. On handling that message consume all queued items from the GUI thread at once. – mghie Jul 07 '13 at 08:24