[DUG] Best way to make this thread safe

Trevor Jones trevorj at ihug.co.nz
Thu May 17 20:38:57 NZST 2007


If you're brave, you can use a helper thread to make the whole operation
thread-safe.  That fails the "use as little extra code as possible"
requirement, but is a surprisingly powerful technique and not as difficult
as you might think.

Using synchronize methods in your worker threads can lead to deadlocks and
often turns the resulting implementation into one that effectively just
serializes everything and eliminates any benefits you might gain from using
multiple threads in the first place.

What you can do is create:
 TLoggerThread = class(TThread) and keep it inside the implementation
section of your logging unit.  Use the unit initialization to create it, and
the finalization section of the unit to terminate it.

You then get your WriteLog procedure to redirect to a matching procedure
inside the TLoggerThread.

So your WriteLog method becomes

Procedure WriteLog(sString : string; opt : LogOption);
Begin
  MyLoggerThread.WriteLog(sString,opt);
End;

Your TLoggerThread.WriteLog is only a little more complex:

Procedure TLoggerThread.WriteLog(sString : string; opt : LogOption);
Begin
  EnterCriticalSection(cs); 
  { where cs is a critical section created and owned by the TLoggerThread }
  Try
     LogList.Add(TLogItem.create(sString,opt));
     { where LogList is a list of TLogItems owned by the TLoggerThread,
       And a TLogItem is a class that has a string and an opt }
     Windows.SetEvent(WakeUp);
     { where WakeUp is a regular Windows Event object created and 
       owned by the TLoggerThread }
  Finally
    LeaveCriticalSection(cs);
    End; 
End;

This still doesn't get any logging done, but it means that your threads that
call WriteLog don't wait on the application's main thread, nor on the
LoggerThread.  The info is just stashed away for subsequent processing.

The fun part happens in your TLoggerThread.ExecuteMethod:

Procedure TLoggerThread.Execute;
Var
  Events : array[0..1] of THandle;
  Signal : integer; 
Begin
  Events[0] := DieEvent; { another regular windows event,
                           Created and owned by the TLoggerThread }
  Events[1] := WakeUp;

  While not terminated do
    Begin
    Signal := Windows.WaitForMultipleObjects(2, at Events,false,INFINITE);
    If Signal = WAIT_ABANDONED then  
      Begin
      Terminate;
      Exit;
      End;
    { This will happen if the application is being torn down as a result
      Of an end-task or similar }
    Signal := Signal - WAIT_OBJECT_0;
    { Signal should now be zero if the thread has been told to die
      Or 1 if it has been told to wake up }
    If Signal = 0 then
      Begin
      Terminate;
      Exit;
      End;
    { at this point, you probably have something to log }
    Synchronize(DoTheWork);
    { This method switches back to the application's main thread
      So it is safe to work with the VCL components, but you still 
      Haven't blocked the callers to your WriteLog procedure. }
    End;
End;
    
Procedure TLoggerThread.DoTheWork;
Var
  LogItem : TLogItem;
Begin
EnterCriticalSection(cs);
Try
  While LogList.Count > 0 do
    Begin
    LogItem := TLogItem(LogList[0]);
    With frmMain.reLog do
      begin
      If lines.Count > 200 then lines.Delete(0);

      lines.add(LogItem.sString);
      SelStart := length(text) - (length(LogItem.sString)+2);
      SelLength := length(LogItem.sString);
      Case LogOption(LogItem.opt) of
        loStart   : begin SelAttributes.Style := [fsbold]; 
SelAttributes.Color := clBlue;   end;
        loNormal  : begin SelAttributes.Style := [];       
SelAttributes.Color := clBlack;  end;
        loError   : begin SelAttributes.Style := [fsbold]; 
SelAttributes.Color := clRed;    end;
        loFinished: begin SelAttributes.Style := [fsbold]; 
SelAttributes.Color := clBlue; lines.add('');  end;
      End;
      SelStart := Length(Text);
      Perform(EM_SCROLLCARET, 0, 0);
      end;
    LogItem.Free;
    LogList.Delete(0);
    End;
  Finally
    LeaveCriticalSection(cs);
    End;
End;

The last thing you need to do with your TLoggerThread is override the
Terminate method.  

Procedure TLoggerThread.Terminate;
Begin
  Windows.SetEvent(DieEvent);
  Inherited Terminate;
End;

There are a few other details, such as overriding the constructor of TThread
in your TLoggerThread so that you create the Windows events and the list,
and overriding the destructor to call closeHandle on the Windows events and
to destroy the list, but the guts of it is there.

Don't worry that the TThread.Terminate method is not virtual, as long as in
your unit's finalization section you call the Terminate method of the
TLoggerThread (and not the Terminate method of a TThread), it will work.

Sorry, it is probably a lot of code to achieve what seems to be a simple
task, but it should be pretty safe and avoid many of the pitfalls of having
multiple threads trying to access the UI simultaneously.

Trevor


-----Original Message-----
From: delphi-bounces at delphi.org.nz [mailto:delphi-bounces at delphi.org.nz] On
Behalf Of Nick
Sent: Thursday, 17 May 2007 9:08 a.m.
To: NZ Borland Developers Group - Delphi List
Subject: [DUG] Best way to make this thread safe

I got a function here I use for logging to a richedit on my main form 
(this is just in functions unit)

Procedure WriteLog(sString : string; opt : LogOption);
Begin
  With frmMain.reLog do
    begin
      If lines.Count > 200 then lines.Delete(0);

      lines.add(sString);
      SelStart := length(text) - (length(sString)+2);
      SelLength := length(sString);
      Case LogOption(opt) of
        loStart   : begin SelAttributes.Style := [fsbold]; 
SelAttributes.Color := clBlue;   end;
        loNormal  : begin SelAttributes.Style := [];       
SelAttributes.Color := clBlack;  end;
        loError   : begin SelAttributes.Style := [fsbold]; 
SelAttributes.Color := clRed;    end;
        loFinished: begin SelAttributes.Style := [fsbold]; 
SelAttributes.Color := clBlue; lines.add('');  end;
      End;
      SelStart := Length(Text);
      Perform(EM_SCROLLCARET, 0, 0);
    end;
End;

Now I want to use that function in my threads to log things. However if 
I call it just like this
  WriteLog('Failed to connect! DB Error', loError);
I could run into access violations
But not quite sure the best way to do this to make it thread safe and 
use as little extra code as possible..

Should I create my own function like this
  WriteLogThread('Failed to connect! DB Error', loError);
in my TThread
and then in that procedure just do
criticalsection start
  WriteLog(message, st);  //this calls the function in my general 
functions unit
criticalsection end
or could I just use critical sections in my functions unit around the 
procedure.

I hope that makes sense =)
Thanks guys.
_______________________________________________
NZ Borland Developers Group - Delphi mailing list
Post: delphi at delphi.org.nz
Admin: http://delphi.org.nz/mailman/listinfo/delphi
Unsubscribe: send an email to delphi-request at delphi.org.nz with Subject:
unsubscribe


More information about the Delphi mailing list