[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