Multi thread safe event in Delphi - multithreading

I'm beginner in Delphi programing.
I use a thread to communicate with my server and I want to pass a Tevent to my thread at creation time and use it to signal a task on this thread from main thread and finally on thread clear the event. This event set in main thread to signal the task on my net thread and finally clear after task completed in the net thread.
I use this line to create thread on run time. All work fine but after adding event to my code rise a problem.
Net_thread:= TNetThread.Create(user, password, TheCallback, Tevent);
TNetThread is my thread class on other unit and Net_thread is my net thread.
TheCallback is a procedure for change UI from thread. Declare this type in TNetThread.
user, password are login data collected in GUI.
Tevent is a handle to my event created in main thread and pass it to Net_thread.
Before I add event to my code I only pass 2 string and a procedure to thread and I have no problem. ...Create(user, password, TheCallback); after add event to my code and pass it as THandle to my thread can not use it. Its like a Cardinal variable and when I try to check its state with this code:
System.SyncObjs.TEvent.WaitFor(FEvent)
I have an error e2076. FEvent set on constructor TNetThread.Create and equal to Tevent received from main thread.
Please give me a simple example?
This is my minimal code:
on main form:
procedure TMainform.FormCreate(Sender: TObject);
var
T_event: THandle;
begin
T_event: := CreateEvent(nil, True, False, nil);
Net_thread:= TNetThread.Create(user, password, TheCallback, T_event);
end;
procedure TMainform.TheCallback(const st,h : String);
begin
//recive data from net thread
end;
on event in main thread
SetEvent(T_event);
And on other unit
type
TMyCallback = procedure(const st, h : String) of object;
TNetThread = class(TThread)
IdTCPClient1: TIdTCPClient;
private
FCallback : TMyCallback;
FEvent: THandle;
protected
procedure execute; override;
procedure SendLog(st, h: string);
public
constructor Create(user_n, psw: string ;aCallback : TMyCallback ; const AEvent: THandle);
end;
constructor TNetThread.Create(user_n, psw: string ;aCallback: TMyCallback; const AEvent: THandle);
begin
inherited Create(false);
FCallback := aCallback;
FEvent := AEvent;
user_name := user_n;
password:= psw;
FreeOnTerminate := true;
end;
procedure TNetThread.SendLog(st ,h: string);
begin
if not Assigned(FCallback) then
Exit;
Self.Queue( // Executed later in the main thread
procedure
begin
FCallback(st, h);
end
);
end;
procedure TNetThread.Execute;
begin
.
.
if (System.SyncObjs.TEvent.WaitFor(FEvent) = wrSignaled) then...
.
.
end;

Related

Settting VCL controls properties from TThread.DoTerminate

I'm using the TThread.DoTerminate method to notify to the main thread which the TThread has terminated. but as soon try to change the properties of some controls (buttons) from inside of the DoTerminate both controls just disappear of the form.
Also when I close the Form I'm getting this message
Project ProjectTest.exe raised exception class EOSError with message
'System Error. Code: 1400. Invalid window handle'.
This is a sample application to reproduce the issue.
type
TFooThread = class;
TFormSample = class(TForm)
Button1: TButton;
Button2: TButton;
Button3: TButton;
ProgressBar1: TProgressBar;
procedure FormCreate(Sender: TObject);
procedure Button1Click(Sender: TObject);
procedure FormDestroy(Sender: TObject);
private
FooThread : TFooThread;
procedure ThreadIsDone;
public
end;
TFooThread = class(TThread)
private
FForm : TFormSample;
protected
procedure DoTerminate; override;
public
procedure Execute; override;
constructor Create(AForm : TFormSample); reintroduce;
destructor Destroy; override;
end;
var
FormSample: TFormSample;
implementation
{$R *.dfm}
{ TFooThread }
constructor TFooThread.Create(AForm: TFormSample);
begin
inherited Create(False);
FreeOnTerminate := False;
FForm := AForm;
end;
destructor TFooThread.Destroy;
begin
inherited;
end;
procedure TFooThread.DoTerminate;
begin
FForm.ThreadIsDone;
inherited;
end;
procedure TFooThread.Execute;
var
i : Integer;
begin
for i := 1 to 100 do
begin
Synchronize(
procedure
begin
FForm.ProgressBar1.Position := i;
end
);
Sleep(50);
end;
Terminate();
end;
{ TFormSample }
procedure TFormSample.Button1Click(Sender: TObject);
begin
FooThread := TFooThread.Create(Self);
TButton(Sender).Enabled := false;
end;
procedure TFormSample.FormCreate(Sender: TObject);
begin
FooThread := nil;
Button3.Visible := False;
end;
procedure TFormSample.FormDestroy(Sender: TObject);
begin
if (FooThread<>nil) then
begin
if not FooThread.Terminated then
FooThread.WaitFor;
FooThread.Free;
end;
end;
procedure TFormSample.ThreadIsDone;
begin
//this code is executed but the controls are not updated
//both buttons just disappear from the form !!!!
//Also if I remove these lines, no error is raised.
Button2.Visible := False;
Button3.Visible := True;
end;
end.
The question is : How I can update the properties of some VCL control as soon the TThread is finished?
It should be fine to update controls inside DoTerminate (as you are).
DoTerminate runs in the context of the thread. Therefore it is not safe to update controls from that method. The base implementation synchronises a call to the OnTerminate event.
So OnTerminate is already synchronised. And it will be safe to update controls from an OnTerminate event handler.
However, I would be more inclined to not have code inside the thread class calling the form because this creates a circular dependency. Rather have the form assign a handler for the OnTerminateevent. This way code that controls the form will be in the form class. You can do the same with the control updates to indicate thread progress.
FooThread := TFooThread.Create(...);
//WARNING: If you need to do **any**
//initialisation after creating a
//thread, it's better to create it
//in a Suspended state.
FooThread.OnTerminate := ThreadIsDone;
//Of course you'll have to change the signature of ThreadIsDone accordingly.
FooThread.OnProgress := ThreadProgress;
//You'd have to define a suitable callback event on the thread.
//Finally, if the thread started in a suspended state, resume it.
FooThread.Start;
Avoiding circular dependencies is a little more work, but greatly simplifies an application.
David mentions that you can create your thread in a running state. To do so safely you must:
Pass all necessary initialisation information into the constructor.
And inside the constructor perform all initialisation before calling the inherited constructor.
Also you have a mistake in your Execute method:
procedure TFooThread.Execute;
var
i : Integer;
begin
...
Terminate(); //This is pointless.
//All it does is set Terminated := True;
end;
The thread terminates when it exits. All the call to Terminate does is set an internal flag to indicate the thread should terminate. You'd normally write your Execute method as follows:
begin
while not Terminated do
begin
...
end;
end;
Then your form might have a button which calls: FooThread.Terminate();
This will cause your while loop to exit at the end of the current iteration. This allows the thread to exit "gracefully".

Delphi: Verify DataSnap connection via TThread

We have an application in which the user can talk to us, it works fine, he create a new conversation, we chat, and that's ok. But, before start chatting, he needs to connect to the DataSnap Server, and that's where I'm trying to make a Thread. Every 5min, a timer would trigger his event to create the Thread and try to connect on the server, as below:
My Thread:
unit UThreadSnapConnection;
interface
uses
System.Classes, System.SysUtils, Data.SqlExpr;
type
TThreadSnapConnection = class(TThread)
private
FSnap: TSQLConnection;
procedure TryToConnect;
protected
procedure Execute; override;
constructor Create;
public
DMSnap: TSQLConnection;
HostName: String;
Port: String;
end;
implementation
{ TThreadSnapConnection }
constructor TThreadSnapConnection.Create;
begin
inherited Create(True);
FreeOnTerminate := True;
end;
procedure TThreadSnapConnection.TryToConnect;
begin
try
FSnap := DMSnap.CloneConnection;
FSnap.Connected := False;
try
FSnap.Connected := True;
except
end;
if FSnap.Connected then
DMSnap.Connected := True;
finally
FreeAndNil(FSnap);
end;
end;
procedure TThreadSnapConnection.Execute;
begin
Synchronize(TryToConnect);
end;
end.
My Timer:
procedure TMyDataModuleSnap.TimerSnapTimer(Sender: TObject);
var
MyThread: TThreadSnapConnection;
begin
if not(MySQLConnection.Connected) then
begin
MyThread := TThreadSnapConnection.Create;
MyThread.DMSnap := MySQLConnection;
MyThread.HostName := 'localhost';
MyThread.Port := '211';
MyThread.Resume;
end;
end;
What I'm doing is an attempt to connect to the server, if it works, then it will make my data module connect.
My problem is, everytime the line
FSnap.Connected := True;
execute it freezes for 1~2 seconds the application, and the reason I made a thread was to not freeze. As long as I know, it should not bother at all the application, so I started to think maybe it's the work it does when setting the Connected property to True, which will freeze independent if it's thread or not.
Is there any way to not freeze when trying to connect?
And this is my first thread and maybe I just misunderstood things and that's not how thread works, but well, if it is not then I need to know, or at least understand what I'm doing wrong with it.
EDIT: The test I'm doing is, I start the application without starting the server, so it will try to connect unsuccessful, and my data module will not connect too.
There are two options:
while the OnTimer event of a TTimer is executed in the thread which has created the timer, you may consider to create the instance outside the main thread
you may consider to use a TThread class instance
The following applies to the #2.
Using a TEvent in the Execute procedure of your thread you can wait for an amount of FInterval time before the execution of the next block of code.
When the Terminated property is set to True, this approach allows the Execute method to immediately return also during the interval count unlike the adoption of a TThread.Sleep(FInterval); call which would freeze the thread itself for the amount of time specified.
The main thread can be optionally notified using a TNotifyEvent when done.
TMyThread = class(TThread)
private
FInterval: Integer;
FTerminateEvent: TEvent;
protected
procedure Execute; override;
procedure TerminatedSet; override;
public
OnEndJob: TNotifyEvent;
constructor Create(Interval: Cardinal; CreateSuspended: Boolean);
destructor Destroy; override;
end;
constructor TMyThread.Create(Interval: Cardinal; CreateSuspended: Boolean);
begin
inherited Create(CreateSuspended);
FInterval := Interval;
FTerminateEvent := TEvent.Create(nil, False, False, '');
end;
destructor TMyThread.Destroy;
begin
FTerminateEvent.Free;
inherited;
end;
procedure TMyThread.TerminatedSet;
begin
inherited;
FTerminateEvent.SetEvent;
end
procedure TMyThread.Execute;
begin
while not Terminated do begin
//do your stuff
//notify your connection to the main thread if you want
if Assigned(OnEndJob) then
Synchronize(procedure
begin
OnEndJob(Self);
end);
//wait fo some amount of time before continue the execution
if wrSignaled = FterminateEvent.WaitFor(FInterval) then
Break;
end;
end;
Don't synchonize the code you want to be executed in a thread: in Delphi a syncronized block is always executed in the calling thread.
I would have preferred to post a comment rather than an answer, but lack the reputation points; something worth considering when reading the following.
Reading between the lines, it looks like you have a connection to a local SQL server. Access is infrequent causing the connection to drop, so you've instituted a timer to check every 5 mins and re-establish the connection if necessary.
This worked, but you found that the connection attempt blocks program execution until it is established, and so you want to move this operation to a worker thread.
As stated by fantaghirocco, Synchronize causes code to run within the main program thread. My understanding is this code runs after all messages in the main thread have been processed, so you could achieve the same result by having the timer post a message, and the associated message handler call TryToConnect (TryToConnect declared in the main form in this case).
Synchronize is the easiest means of allowing threads to interact with the main thread without having to worry about two or more threads accessing the same object at the same time.
To prevent the connection process from blocking the main program thread, the MySQLConnection Connected property would have to be set in the Execute method of the TThread descendant (not encapsulated within a call to Synchronize).
But doing so introduces the risk of the worker thread and main program accessing MySQLConnection at the same time. To protect against this you would need to introduce a critical section, or similar. If unfamiliar, then check TCriticalSection in the RAD Studio help; there's a section on Critical Sections and an example.
Both the main program and thread would then encapsulate any calls to MySQLConnection within a critical section try finally block:
FLock.Acquire;
try
{code accessing MySQLConnection goes here}
finally
FLock.Release;
end;
Where FLock is a TCriticalSection object.
Any thread attempting to acquire FLock while already acquired by another, will be blocked until FLock is released. This means the main thread would only be blocked if the user attempted access to MySQLConnection when the worker thread was already attempting a connection.
Update:
To get you started, the following is a simple program consisting of two units; Unit1 contains the main form (what you're presented with when you create a new application). The second unit, Unit2 contains a thread. I've done it this way since your thread appears to be in a separate unit.
I've added a button and a critical section to TForm1 (add System.SyncObjs to the uses clause). In the click event of Button1 I create an instance of TMyThread (in your code this would be handled by the timer event):
type
TForm1 = class(TForm)
Button1: TButton;
procedure FormCreate(Sender: TObject);
procedure FormDestroy(Sender: TObject);
procedure Button1Click(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
FLock: TCriticalSection;
end;
var
Form1: TForm1;
implementation
uses Unit2;
{$R *.dfm}
procedure TForm1.Button1Click(Sender: TObject);
begin
TMyThread.Create;
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
FLock := TCriticalSection.Create;
end;
procedure TForm1.FormDestroy(Sender: TObject);
begin
FLock.Free;
end;
Unit2 contains the thread. The execute method is a fire once and finish effort. Unit1 is added to the uses clause in the implementation to give the code access to the Form1 variable:
type
TMyThread = class (TThread)
protected
procedure Execute; override;
public
constructor Create;
end;
implementation
uses Unit1;
{ TMyThread }
constructor TMyThread.Create;
begin
inherited Create (False);
end;
procedure TMyThread.Execute;
begin
with Form1 do begin
FLock.Acquire;
try
{access MySQLConnection methods here}
finally
FLock.Release;
end;
end;
end;
When you run this simple program and click Button1, a separate thread is created and the execute method run, following which the thread is destroyed. This process is repeated every time you click Button1.
If you place a breakpoint in Unit1 on the MyThread := TMyThread.Create line, and another breakpoint in Unit2 on the FLock.Acquire line, run the program and click Button1, the code will stop in the main thread; the thread Id shown in the left hand pane.
If you click F9 to continue program execution, it will then stop on the Unit2 breakpoint. You'll note the thread Id is now different, and the Thread Status window down the bottom of the IDE now lists this extra thread. When you press F9 again and this new thread disappears.
This program does nothing, but you would place whatever MySQLConnection code you needed to run in this thread where I have the comment in the Try Finally block.
In the main thread, wherever MySQLConnection's methods are accessed, you will also need to encapsulate these within a FLock try finally block. For example, if you have a TClientDataSet connected to a TDataSetProvider connected to a TSQLDataSet connected to your MySQLConnection, then opening the TClientDataSet will have to be encapsulated within this FLock Try Finally:
begin
FLock.Acquire;
try
CDS.Open;
finally
FLock.Release;
end;
end;
Where CDS is the TClientDataSet.
The code you intend to run in the thread basically closes the connection and re-opens it. A side benefit of the critical section (if properly configured, and all access to MySQLConnection protected by the critical section), is it will prevent the connection being shut in the middle of a user's query.

Problem using Synchronize

I need to execute a function in a separated thread and wait until the thread is finished.
For example, here's the original function :
Procedure Search;
begin
CallA;
CallB;
end;
This is the modified function :
Procedure Search;
var
testMyThread: TMyThread;
Done: Boolean;
begin
// create a new thread to execute CallA
testMyThread:=TMyThread.Create(False,Done);
WaitForSingleObject(testMyThread.Handle, INFINITE );
if not Done then
begin
TerminateThread(testMyThread.Handle, 0);
end
else;
CallB;
end
unit uMyThread;
interface
uses classes;
type
TMyThread = class(TThread)
private
{ Private declarations }
FDone: ^boolean;
protected
procedure Execute; override;
public
constructor Create(const aSuspended: boolean; var Done: boolean);
procedure CallA;
end;
implementation
uses uMain;
constructor TMyThread.Create(const aSuspended: boolean;
var Done: boolean);
begin
inherited Create(aSuspended);
FDone := #Done;
end;
procedure TMyThread.CallA;
begin
// enumurating several things + updating the GUI
end;
procedure TMyThread.Execute;
begin
inherited;
Synchronize(CallA); // << the problem
FDone^ := true;
end;
end.
Could you tell me why the thread code above doesn't work (CallA never being executed) if I use Synchronize inside TMyThread.Execute ?
Because Synchronize will call a method within application's message loop. And using WaitForSingleObject you simply put all application on hold. Try this:
Procedure Search;
var
testMyThread: TMyThread;
Done: Boolean;
begin
// create a new thread to execute CallA
testMyThread:=TMyThread.Create(False,Done);
while (not Done) and (not Application.Terminated) do
Application.ProcessMessages;
if not Application.Terminated then
CallB;
end
the Delphi tthread class has an event called onThreadTerminate.
This is called in the context of the application thread, when the thread leaves the execute method.
you can use this event in your application.

How can a thread notify an object that doesn't have a window handle?

I'm new to multithreading, but not a complete novice. I need to perform a call to a webservice in a worker thread.
In the main thread I have a form (TForm) with a private data member (private string) that only the worker thread will write to (I pass the a pointer to it into the thread before it resumes). When the worker thread has finished its webservice call and written the resultant response xml to the private member on the form, the worker thread uses PostMessage to send a message to the form's handle (which I also passed into the thread before it resumed).
interface
const WM_WEBSERVCALL_COMPLETE = WM_USER + 1;
type
TWebServiceResponseXML = string;
PWebServiceResponseXML = ^TWebServiceResponseXML;
TMyForm = class(TForm)
...
private
...
fWorkerThreadID: Cardinal;
fWebServiceResponseXML: TWebServiceResponseXML;
public
...
procedure StartWorkerThread;
procedure OnWebServiceCallComplete(var Message: TMessage); Message WM_WEBSERVCALL_COMPLETE;
end;
TMyThread = class(TThread)
private
protected
procedure Execute; override;
public
SenderHandle: HWnd;
RequestXML: string;
ResponseXML: string;
IMyService: IService;
PResponseXML: PWebServiceResponseXML;
end;
implementation
procedure TMyForm.StartWorkerThread;
var
MyWorkerThread: TMyThread;
begin
MyWorkerThread := TMyThread.Create(True);
MyWorkerThread.FreeOnTerminate := True;
MyWorkerThread.SenderHandle := self.Handle;
MyWorkerThread.RequestXML := ComposeRequestXML;
MyWorkerThread.PResponseXML := ^fWebServiceResponseXML;
MyWorkerThread.Resume;
end;
procedure TMyForm.OnWebServiceCallComplete(var Message: TMessage);
begin
// Do what you want with the response xml string in fWebServiceResponseXML
end;
procedure TMyThread.Execute;
begin
inherited;
CoInitialize(nil);
try
IMyService := IService.GetMyService(URI);
ResponseXML := IMyService.Search(RequestXML);
PResponseXML := ResponseXML;
PostMessage(SenderHandle, WM_WEBSERVCALL_COMPLETE, 0, 0);
finally
CoUninitialize;
end;
end;
It works great, but now I want to do the same thing from a datamodule (which doesn't have a Handle)... so I would really appreciate some useful code to supplement the working model I have.
EDIT
What I really want is the code (if possible) that would allow me to replace the line
MyWorkerThread.SenderHandle := self.Handle;
with
MyWorkerThread.SenderHandle := GetHandleForThisSOAPDataModule;
I have used this technique before with some success: Sending messages to non-windowed applications
Basically, use a second thread as a message pump on a handle obtained via AllocateHWND. This is admittedly irritating, and you would be better off using a library to handle all the details. I prefer OmniThreadLibrary but there are others - see How Do I Choose Between the Various Ways to do Threading in Delphi? and Delphi - Threading frameworks.
You can allocate you own handle with AllocateHwnd and use that as a PostMessage target.
TTestThread = class(TThread)
private
FSignalShutdown: boolean;
// hidden window handle
FWinHandle: HWND;
protected
procedure Execute; override;
// our window procedure
procedure WndProc(var msg: TMessage);
public
constructor Create;
destructor Destroy; override;
procedure PrintMsg;
end;
constructor TTestThread.Create;
begin
FSignalShutdown := False;
// create the hidden window, store it's
// handle and change the default window
// procedure provided by Windows with our
// window procedure
FWinHandle := AllocateHWND(WndProc);
inherited Create(False);
end;
destructor TTestThread.Destroy;
begin
// destroy the hidden window and free up memory
DeallocateHWnd(FWinHandle);
inherited;
end;
procedure TTestThread.WndProc(var msg: TMessage);
begin
if Msg.Msg = WM_SHUTDOWN_THREADS then
// if the message id is WM_SHUTDOWN_THREADS
// do our own processing
FSignalShutdown := True
else
// for all other messages call
// the default window procedure
Msg.Result := DefWindowProc(FWinHandle, Msg.Msg,
Msg.wParam, Msg.lParam);
end;
You can apply this to anything not just threads. Just beware that AllocateHWND is NOT threade safe as indicated here.
Alternatives based on the use of an event:
Use OnTerminate of the thread (already present) in combination with a flag:
TMyDataModule = class(TDataModule)
private
procedure OnWebServiceCallComplete(Sender: TObject);
...
TMyThread = class(TThread)
public
property TerminateFlag: Integer ...
...
procedure TMyDataModule.StartWorkerThread;
...
MyWorkerThread.OnTerminate := <Self.>OnWebServiceCallComplete;
...
procedure TMyDataModule.OnWebServiceCallComplete(Sender: TObject);
begin
if MyWorkerThread.TerminateFlag = WEBCALL_COMPLETE then
...
end;
Set the TerminateFlag in the Execute routine. OnTerminate will automatically fire, even if FreeOnTerminate is True.
Add a new event property to the thread class in which you may provide the flag as a parameter to indicate termination/thread result. Something like shown here. Be sure to synchronize the event call. Or forget the parameter and just only call the event if execution completed gracefully (like you're doing now).

Execute thread on some object property change

I make a logging application and I have a LogEvent object with some string properties on it. I want to make this logging asynchronous and in another thread for not blocking the applications GUI thread.
Idea is that when I start application, some LogEventThread is running on the background all the time. If LogEvent property has changed then thread is executed, after execution thread suspends and waits another LogEvent object property change and run it again if new property change is captured.
Which are the best practises to design this?
EDIT:
I created an example. Please tell me if I'm on the correct path.
I have a Form1:
unit MainWindow;
interface
uses Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls, TrackEventSenderThread, Generics.Collections, TrackEvent;
type
TForm1 = class(TForm)
btnTest: TButton;
procedure FormCreate(Sender: TObject);
procedure btnTestClick(Sender: TObject);
private
teqTrackEventSenderThread: TTrackEventSenderThread;
trackEventQueue: TThreadedQueue<TTrackEvent>;
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
procedure TForm1.btnTestClick(Sender: TObject);
var
trackEvent: TTrackEvent;
begin
trackEvent := TTrackEvent.Create;
trackEvent.Category := 'test';
trackEvent.Action := 'test';
trackEventQueue.PushItem(trackEvent);
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
trackEventQueue := TThreadedQueue<TTrackEvent>.Create;
teqTrackEventSenderThread := TTrackEventSenderThread.Create(True);
teqTrackEventSenderThread.TrackEventQueue := trackEventQueue;
teqTrackEventSenderThread.Start;
end;
end.
TrackEvent class:
unit TrackEvent;
interface
type
TTrackEvent = class(TObject)
private
sCategory: string;
sAction: string;
public
property Category: string read sCategory write sCategory;
property Action: string read sAction write sAction;
end;
implementation
end.
And thread class:
unit TrackEventSenderThread;
interface
uses Classes, Generics.Collections, TrackEvent;
type
TTrackEventSenderThread = class(TThread)
private
trackEvent: TTrackEvent;
teqTrackEventQueue: TThreadedQueue<TTrackEvent>;
public
constructor Create(CreateSuspended: Boolean);
property TrackEventQueue: TThreadedQueue<TTrackEvent> read teqTrackEventQueue write teqTrackEventQueue;
protected
procedure Execute; override;
end;
implementation
constructor TTrackEventSenderThread.Create(CreateSuspended: Boolean);
begin
inherited;
end;
procedure TTrackEventSenderThread.Execute;
begin
while not Terminated do
begin
if teqTrackEventQueue.QueueSize > 0 then
begin
trackEvent := teqTrackEventQueue.PopItem;
//send data to server
end;
end;
end;
end.
You can build a thread-safe Queue class which is used in a Producer-Consumer model. Your TThread descendant class should own an instance of this Queue class.
When you start your application, your queue is empty, and your logging thread is blocked waiting for queue. When you push a new string into the queue from the main thread, your queue pulses the logging thread, your logging thread wakes up and pops items from the queue until the queue is empty again.
To implement the queue in Delphi 2010, you can use TQueue generic class as the base type, and use System.TMonitor for synchronization. In Delphi XE, there is already a class which implements this for you, named TThreadedQueue. So If you are using Delphi XE, create an instance of TThreadedQueue, and in your logging thread try to call its PopItem() method.
EDIT:
Here is a sample logging thread which receives string logs:
unit uLoggingThread;
interface
uses
SysUtils, Classes, Generics.Collections, SyncObjs {$IFDEF MSWINDOWS} , Windows {$ENDIF};
type
TLoggingThread = class(TThread)
private
FFileName : string;
FLogQueue : TThreadedQueue<string>;
protected
procedure Execute; override;
public
constructor Create(const FileName: string);
destructor Destroy; override;
property LogQueue: TThreadedQueue<string> read FLogQueue;
end;
implementation
{ TLoggingThread }
constructor TLoggingThread.Create(const FileName: string);
begin
inherited Create(False);
FFileName := FileName;
FLogQueue := TThreadedQueue<string>.Create;
end;
destructor TLoggingThread.Destroy;
begin
FLogQueue.Free;
inherited;
end;
procedure TLoggingThread.Execute;
var
LogFile : TFileStream;
FileMode : Word;
ALog : string;
begin
NameThreadForDebugging('Logging Thread');
// FreeOnTerminate := True;
if FileExists(FFileName) then
FileMode := fmOpenWrite or fmShareDenyWrite
else
FileMode := fmCreate or fmShareDenyWrite;
LogFile := TFileStream.Create(FFileName,FileMode);
try
while not Terminated do
begin
ALog := FLogQueue.PopItem;
if (ALog <> '') then
LogFile.Write(ALog[1],Length(ALog)*SizeOf(Char));
end;
finally
LogFile.Free;
end;
end;
end.
This TThread descendant uses a TThreadedQueue object as a buffer. When FLogQueue.PopItem is called, if the queue is empty, the thread goes to sleep, and waits until something is pushed into the queue. When an item is available in the queue, the thread pops it, and writes it to a file. This is a very simple code to just let you understand the basics of what you should do.
And here is a sample code for a form which is running in the context of main thread, and is logging a sample message:
unit fMain;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls, uLogginThread;
type
TfrmMain = class(TForm)
btnAddLog: TButton;
procedure FormCreate(Sender: TObject);
procedure FormDestroy(Sender: TObject);
procedure btnAddLogClick(Sender: TObject);
private
FLoggingThread : TLoggingThread;
public
{ Public declarations }
end;
var
frmMain: TfrmMain;
implementation
{$R *.dfm}
procedure TfrmMain.FormCreate(Sender: TObject);
begin
FLoggingThread := TLoggingThread.Create(ExtractFilePath(Application.ExeName) + 'Logs.txt');
end;
procedure TfrmMain.FormDestroy(Sender: TObject);
begin
FLoggingThread.Terminate;
FLoggingThread.LogQueue.DoShutDown;
FLoggingThread.WaitFor;
FreeAndNil(FLoggingThread);
end;
procedure TfrmMain.btnAddLogClick(Sender: TObject);
begin
FLoggingThread.LogQueue.PushItem('This is a test log. ');
end;
end.
Here an instance of TLoggingThread is created when the form is initialized. When you press btnAddLog, a sample message is sent to the logger thread via its LogQueue property.
Take note of how the thread is terminated in FormDestroy method. First the thread is signaled that it is terminated, then we tell LogQueue to release any lock, so if the logger thread is waiting for the queue, it will wake up automatically after calling DoShutDown. Then we wait for the thread to finish up by calling WaitFor method, and eventually we destroy the thread instance.
Good Luck
In a multi-threaded application, use
TEvent to allow one thread to signal
to other threads that an event has
occurred.
http://docwiki.embarcadero.com/VCL/en/SyncObjs.TEvent
I would use a Queue of strings with a critical section inside push() and pop(). Inside the thread I would pop strings off, and log them. Inside the GUI thread I would push strings on the queue. I have done something similar before, and it is simple to implement.
Edit
Interface:
TThreadSafeQueue = class(TQueue)
protected
procedure PushItem(AItem: Pointer); override;
function PopItem: Pointer; override;
function PeekItem: Pointer; override;
end;
var
CRITICAL_SECTION: TCriticalSection;
Implementation:
function TThreadSafeQueue.PeekItem: Pointer;
begin
CRITICAL_SECTION.Enter;
Result := inherited PeekItem;
CRITICAL_SECTION.Leave;
end;
function TThreadSafeQueue.PopItem: Pointer;
begin
CRITICAL_SECTION.Enter;
Result := inherited PopItem;
CRITICAL_SECTION.Leave;
end;
procedure TThreadSafeQueue.PushItem(AItem: Pointer);
begin
CRITICAL_SECTION.Enter;
inherited PushItem(AItem);
CRITICAL_SECTION.Leave;
end;
Initialization
CRITICAL_SECTION := TCriticalSection.Create;
Finalization
FreeAndNil(CRITICAL_SECTION);
This code uses pointers to objects, but you can create storage for your strings inside the object, using a stringlist or array or whatever best fits your purpose, and change the pop and push methods to operate on your own storage.
Edit
Something like this:
procedure TMyThread.Execute;
var
Msg: string;
begin
while not Terminated do
begin
if FQueue.Count > 0 then
begin
Msg := FQueue.pop();
PerformLog(Msg); {Whatever your logging method is}
end;
Sleep(0);
end;
end;

Resources