VCL sources is a very interesting reading, if you have a time for it. Many Delphi programmers (like me) use VCL TThread class as a simple wrapper for WinAPI kernel thread object, avoiding to use Synchronize and Queue methods by using a custom message processing instead. Still it is worthwhile to understand the inner workings of VCL thread synchronization.
Disclaimer: I am currently using Delphi 2009, so some details of the next study may be different for the other Delphi (VCL) versions.
TThread class affords two ways for a background (“worker”) thread to execute a code in context of the main (GUI) thread. You can use either one of the Synchronize method overloads which force the worker thread into a wait state until the code is executed, or else you can use one of the Queue method overloads that just put the code into the main thread’s synchronization queue and allows a worker thread to continue. The definitions of the Synchronize and Queue methods in TThread class are:
type TThreadMethod = procedure of object; TThreadProcedure = reference to procedure; procedure Synchronize(AMethod: TThreadMethod); overload; procedure Synchronize(AThreadProc: TThreadProcedure); overload; class procedure Synchronize(AThread: TThread; AMethod: TThreadMethod); overload; class procedure Synchronize(AThread: TThread; AThreadProc: TThreadProcedure); overload; procedure Queue(AMethod: TThreadMethod); overload; procedure Queue(AThreadProc: TThreadProcedure); overload; class procedure Queue(AThread: TThread; AMethod: TThreadMethod); overload; class procedure Queue(AThread: TThread; AThreadProc: TThreadProcedure); overload;
We see that Synchronize and Queue methods can be used either with ordinary methods (TThreadMethod) or anonymous methods (TThreadProcedure).
Both Synchronize and Queue methods internally call a private Synchronize method overload, which is examined in detail next.
class procedure TThread.Synchronize(ASyncRec: PSynchronizeRecord; QueueEvent: Boolean = False); overload;
The first argument is a pointer to TSynchronizeRecord structure (see definition below) which in turn contains a pointer to a code to be executed; a code pointer can be either ordinary method pointer (TThreadMethod) or anonymous method’s reference (TThreadProcedure)
type PSynchronizeRecord = ^TSynchronizeRecord; TSynchronizeRecord = record FThread: TObject; FMethod: TThreadMethod; FProcedure: TThreadProcedure; FSynchronizeException: TObject; end; TSyncProc = record SyncRec: PSynchronizeRecord; Queued: Boolean; Signal: THandle; end;
The second argument (QueueEvent) is False for Synchronize overloads and True for Queue overloads.
Now back to the TThread.Synchronize code:
var SyncProc: TSyncProc; SyncProcPtr: PSyncProc; begin if GetCurrentThreadID = MainThreadID then begin if Assigned(ASyncRec.FMethod) then ASyncRec.FMethod() else if Assigned(ASyncRec.FProcedure) then ASyncRec.FProcedure(); end
The above is a kind of “fool protection”. If Synchronize (or Queue) is called from the main thread, the correspondent code is just executed. The rest of the method’s code is dealing with the worker’s thread calls:
else begin if QueueEvent then New(SyncProcPtr) else SyncProcPtr := @SyncProc;
For Synchronize calls we use stack variable to store TSyncProc structure; we can’t use stack for asynchronous Queue calls, so we allocate a new TSyncProc variable on the heap.
if not QueueEvent then SyncProcPtr.Signal := CreateEvent(nil, True, False, nil) else SyncProcPtr.Signal := 0;
For Synchronize calls we need a signaling event to wake up the worker thread after the main thread have finished executing the synchronized code; for the Queue calls we does not interfere into thread scheduling, so we need not a signaling event.
try EnterCriticalSection(ThreadLock); try SyncProcPtr.Queued := QueueEvent; if SyncList = nil then SyncList := TList.Create; SyncProcPtr.SyncRec := ASyncRec; SyncList.Add(SyncProcPtr); SetEvent(SyncEvent); if Assigned(WakeMainThread) then WakeMainThread(SyncProcPtr.SyncRec.FThread);
We are preparing an information for the main thread about the code to execute. I will discuss how (and where) the main thread finds out that it has some worker code to execute a little later.
if not QueueEvent then begin LeaveCriticalSection(ThreadLock); try WaitForSingleObject(SyncProcPtr.Signal, INFINITE); finally EnterCriticalSection(ThreadLock); end; end;
A very interesting code fragment (executed only for Synchronize calls). We release the ThreadLock critical section (to enable other threads to execute Synchronize or Queue calls), wait the main thread to execute the code and enter the ThreadLock again.
The above code fragment is exactly the job for a condition variable. Condition variable is a synchronization primitive first introduced in Windows Vista (in Windows part of the world; it probably always existed in Unix/Linux). Have VCL supported only Vista and above Windows versions, the above code fragment could be replaced like this:
if not QueueEvent then begin SleepConditionVariableCS(CondVar, ThreadLock, INFINITE); end;
(well, not so simple; we should initialize the CondVar condition variable before we can use it. See msdn for details).
The use of the conditional variables makes the code more effective (thread enters a wait state and releases the specified critical section as an atomic operation) and more readable.
finally LeaveCriticalSection(ThreadLock); end; finally if not QueueEvent then CloseHandle(SyncProcPtr.Signal); end; if not QueueEvent and Assigned(ASyncRec.FSynchronizeException) then raise ASyncRec.FSynchronizeException; end; end;
That is all. Both Synchronize and Queue calls prepare a TSyncProc structure containing a pointer to the code to be executed by the main thread, insert the structure into the global SyncList list, and with Synchronize call a worker thread also enters a wait state until the main thread sets a signaling event in the TSyncProc structure.
Now about how the main thread finds out that the worker threads have prepared TSyncProc structures for him. The answer is WakeMainThread global variable defined in Classes.pas:
var WakeMainThread: TNotifyEvent = nil;
in the above Synchronize procedure we have
if Assigned(WakeMainThread) then WakeMainThread(SyncProcPtr.SyncRec.FThread);
The WakeMainThread is assigned during the application initialization by the following code:
procedure TApplication.WakeMainThread(Sender: TObject); begin PostMessage(Handle, WM_NULL, 0, 0); end;
and WM_NULL message is processed in application window procedure as follows:
procedure TApplication.WndProc(var Message: TMessage); [..] with Message do case Msg of [..] WM_NULL: CheckSynchronize; [..]
We see that every Synchronize or Queue method call posts WM_NULL message to the hidden application window; the WM_NULL message is processed by calling CheckSynchronize procedure. CheckSynchronize procedure scans the global SyncList list of the TSyncProc structures, executes the code pointed by them and, for Synchronize calls only, sets a signaling event in the TSyncProc structures after executing the code to wake up a worker thread.
What is the resume of the above analysis? Both Synchronize and Queue methods internally use window message processing (in the hidden application window), and in fact there is a little difference with custom message processing here. Due to design reasons a custom message processing is preferable when designing multithreaded components (with their own window procedures, created for example by Classes.AllocateHWnd function). Usually there is no practical reason to use custom message processing when designing multithreaded applications – Synchronize and Queue methods of the TThread class are just as good.