Delphi 6, 7 threads synchronization in ActiveX controls

By: Clever Components

Abstract: This article describes how to improve Delphi 6, 7 threads synchronization in ActiveX controls

Abstract

Starting from Delphi 6 the VCL synchronization mechanism underwent big changes. It does not use anymore any hidden window handles in order to organize method calls between Delphi threads. All these changes were necessary for VCL library to be compatible with Kylix version of VCL. However there are situations when you have to perform some additional manipulations to make your code work properly. Please note that Delphi 6 synchronization model will not work when threads used inside of dll or ActiveX control placed onto the Web form. Such behavior is due to using of the CheckSynchronize function for processing of the VCL synchronization queue. If you are writing an exe application this function is called automatically, but if you are developing an ActiveX library there are no any mechanism to do it. Another example, if you are developing some ActiveX control which can be used inside web page. The web page can be opened using Microsoft Internet Explorer (IE) in more than one browser window simultaneously. In contrast to Borland VCL, IE has different working threads per each browser window therefore Delphi synchronization model also will not work inside such multiple windows. All information described above also applies to working with services.

Call CheckSynchronize in dll

If you want to use the VCL synchronization feature, you should provide the CheckSynchronize calls as it needs. The most easiest way to provide your application with asynchronous mode is to assign the WakeMainThread callback routine. This callback is fired when downloader's thread is about to synchronize with the main thread. Next, within the WakeMainThread handler we post a message to the active form's window. Finally the message handler calls the CheckSynchronize to process threads synchronization. The sample below demonstrates using of WakeMainThread routine in a Delphi ActiveX Form:

const 
   WM_CLSYNCHRONIZE = WM_USER + 1;
...
type
   TActiveFormX = class(TActiveForm, IActiveFormX)
      procedure WMclSynchronize(var Message: TMessage);
      message WM_CLSYNCHRONIZE;
... 
procedure TActiveFormX.ActiveFormCreate(Sender: TObject);
begin
   Classes.WakeMainThread := DoOnWakeMainThread;
end;

procedure TActiveFormX.DoOnWakeMainThread(Sender: TObject);
begin
   PostMessage(Self.Handle, WM_CLSYNCHRONIZE, 0, 0);
end;

procedure TActiveFormX.WMclSynchronize(var Message: TMessage);
begin
   Classes.CheckSynchronize();
end;
When creating a new instance of the ActiveX Form, the WakeMainThread event handler is initialized. When the main thread is waked up, the user-defined message is posted to the Form handle and finally CheckSynchronize is called.

Back to the hidden window

The method explained above uses window handles to provide the ability of processing the synchronization queue. Such approach looks not so effective. So we would rather use window handles only instead of using an additional synchronization engine. But neither CheckSynchronize nor Delphi 5 window based synchronization model works if you need to wait for thread completion from the base thread which is not the main application thread. Lets go ahead and take a close look at TThread class in Delphi 5. When new instance of TThread is created, the AddThread procedure is called. It creates the hidden window during it's first call. All other calls are expected to be from the same thread as the first call. Now please take a look at WaitFor method. There is a condition which defines when thread synchronization engine should work - only when you invoke this method from the inside of the main application thread. If not, the message queue will not be processed.

function TThread.WaitFor: LongWord;
begin
...
   if GetCurrentThreadID = MainThreadID then
      while MsgWaitForMultipleObjects(1, H, False, INFINITE, QS_SENDMESSAGE)
         = WAIT_OBJECT_0 + 1 do PeekMessage(Msg, 0, 0, 0, PM_NOREMOVE)
   else WaitForSingleObject(H, INFINITE);
...
end;
As a result, you can not use these methods when developing Windows services and ActiveX controls designed for Web Forms.

Adding true multi-threading synchronization to the VCL

The suggested solution relies on using of the separate window handles per each base thread (with which another threads need to be synchronized). We will need for some generic class which can handle all synchronization windows being used:

TclSynchronizerManager = class
...
   function FindSyncInfo(ASyncBaseThreadID: LongWord): TclSyncInfo;
   procedure AddThread(ASynchronizer: TclThreadSynchronizer);
   procedure RemoveThread(ASynchronizer: TclThreadSynchronizer);
   procedure Synchronize(ASynchronizer: TclThreadSynchronizer);
...
end;

TclThreadSynchronizer = class
...
   property SyncBaseThreadID: LongWord read FSyncBaseThreadID;
...
end;
Each TThread descendant must have its own TclThreadSynchronizer object. When a new TclThreadSynchronizer instance is created, the TclSynchronizerManager.AddThread is called and if there is no windows handle exists for the specified SyncBaseThreadID, new window is created. The rest of synchronization is similar to one which can be found in Delphi 5 except for some minor changes in ThreadWndProc and Synchronize methods. Modified they both must work with corresponding TclThreadSynchronizer instead of using one single hidden window:

procedure TclSynchronizerManager.Synchronize(
   ASynchronizer: TclThreadSynchronizer);
begin
   SendMessage(FindSyncInfo(ASynchronizer.SyncBaseThreadID).FThreadWindow,
      CM_EXECPROC, 0, Longint(ASynchronizer));
end;
Please note that you can not use TThread.WaitFor method to wait for the thread completion. You must use your own one which looks like following:

function TThreadEx.Wait: LongWord;
begin
...
   if GetCurrentThreadID = FSynchronizer.SyncBaseThreadID then
      while MsgWaitForMultipleObjects(1, H, False, INFINITE,
         QS_SENDMESSAGE) = WAIT_OBJECT_0 + 1 do
         PeekMessage(Msg, 0, 0, 0, PM_NOREMOVE)
   else WaitForSingleObject(H, INFINITE);
...
end;
Finally to prevent invalid handle exceptions, the Wait method can be modified using the DuplicateHandle routine:

procedure TThreadEx.Wait;
var
   Msg: TMsg;
   H: THandle;
begin
   DuplicateHandle(GetCurrentProcess(), Handle, GetCurrentProcess(),
      @H, 0, False, DUPLICATE_SAME_ACCESS);
   try
      if GetCurrentThreadID = FSynchronizer.SyncBaseThreadID then
      begin
         while MsgWaitForMultipleObjects(1, H, False, INFINITE,
            QS_SENDMESSAGE) = WAIT_OBJECT_0 + 1 do
         begin
            while PeekMessage(Msg, 0, 0, 0, PM_REMOVE) do
            begin
               DispatchMessage(Msg);
            end;
         end;
      end else
      begin
         WaitForSingleObject(H, INFINITE);
      end;
   finally
      CloseHandle(H);
   end;
end;

Source Code

A full source code of all classes described in this article can be downloaded at DelphiSync.zip This code is constantly being refined and improved and your comments and suggestions are always welcome. Please write us at info@clevercomponents.com

With best regards,
Sergey Shirokov
Clever Components team.
www.clevercomponents.com

Server Response from: ETNASC01