Although I am using Oxygene a lot these days, Delphi remains my tool of choice for Win32 (and x64) development, together with the VCL.
Hence this post.
A long time ago, in a galaxy far far away, Delphi was a Windows only development tool. 16 was the number of the bits with which it was concerned, and 1 was the number of threads in the process (though it was not called a thread).
It was a simpler time.
Then the bits were doubled and the number of threads did multiply, but it was important still that some things happened only on the original thread, though for a time a blinded eye could be turned since – no matter the plurality of threads – with only one piece of silicon to share, only one thread could be running at any one time.
Yet lo, TThread.Synchronize() was conceived.
No More Inner Verity
Ok, that’s enough faux-k lore type language. Back to proper English.
As anyone that ever did multi-threading in those glory days will (or should) tell you, the Synchronize() mechanism had problems rights from the start, especially if you managed to get your hands on a PC with more than one actual CPU. You quickly learned to avoid it like the proverbial.
Those problems were eventually fixed I believe, but by then I had learned to use other techniques and it remained a relatively complex mechanism for achieving what was – on Windows – mostly achievable by far simpler means. Remember, it was not a general purpose thread synchronization mechanism. The sole purpose of Synchronize() was to ensure that some specified method was performed on the application main thread, sometimes referred to as the “VCL thread” because it was primarily where your VCL UI code would run.
These days of course, the Delphi TThread class finds itself co-opted into a role for which it was never originally designed: a cross-platform abstraction of whatever threading capabilities exist on the various supported platforms, with a concomitant explosion in complexity of the class (in terms of trying to understand it from perusing the source at least).
Of course, the “VCL thread” per se doesn’t exist in this new world order, since the VCL remains planted firmly in Windows soil, with TThread serving both masters – VCL and the other guy. You know of whom I speak: Bit of a chimp. With a tendency toward combustion.
But back to Windows.
If you are working on code that is similarly firmly planted in Windows, Synchronize() is massively over-engineered for the purpose it serves. Especially when you consider that Windows already provides a reliable and efficient mechanism for passing control from one thread to another synchronously.
That mechanism is messaging.
I Send a Message
Messages in Windows can be posted (asynchronously) or sended [sic] (synchronously).
If a thread uses
SendMessage() to send a message to a window created on some other thread, the synchronous behaviour is maintained by Windows itself, which will manage the business of suspending the calling thread, processing the message on the thread of the recipient window, and then returning the result to the calling thread.
When you simply wish to notify the main (VCL) thread of some event, this is perfect and – with a bit of additional work – can be extended to pass whatever additional data about that event you wish.
I have created a class which provides the basis for just such a mechanism, where-ever I may need it. Typically this is in some library function which involves creating a worker thread but which supports some sort of VCL thread callback mechanism.
The threading aspects of the implementation are entirely transparent to the consumer of the library function, including the message handling device.
As an example, I shall show the key aspects of a thread implementation calling an event with a string parameter, where that event is to be handled in the VCL thread.
The signature of the event that will be supported will be:
type TStringEvent = procedure(const aString: String) of object;
This is the only aspect of this mechanism that needs to be exposed in the interface of my library unit. The rest is implementation detail that the consumer of that library code need not be concerned with.
Message Handler Implementation
First of all, the mechanism I implemented requires that I derive a class from a TMessageHandler base (which I shall cover in a later post). This is the intermediary that will translate Windows messages sent from my thread(s) into the event notifications supported by those threads.
In this case there is just one message involved. I implement a method to receive and process that message in the VCL thread, and a corresponding method to be called by the thread wishing to send that message.
Also, a single instance of this particular message handler will cater for all threads that might be involved. Everything that the handler will need to translate messages into VCL events will be delivered via the messages themselves, so the declaration of the handler is very simple:
const MSG_OUTPUT = WM_USER; type TOutputHandler = class(TMessageHandler) procedure Output(var aMessage: TMessage); message MSG_OUTPUT; procedure SendOutput(const aThread: TMyThread; const aString: String); end;
An instance of this handler must be created in the VCL thread context in order to function correctly (compliance with this is ensured via assertions in the TMessageHandler base class). The simplest way to ensure this is to create the handler in unit initialization (and destroy it in finalization):
var _OutputHandler: TOutputHandler; initialization _OutputHandler := TOutputHandler.Create; finalization _OutputHandler.Free; end.
Remember, all of this is safely tucked away in the implementation of my unit. _OutputHandler is a unit implementation variable, sometimes (incorrectly imho) called a “global”. The leading underscore in the name is my personal convention in such cases.
The implementation of the methods on the handler class are straightforward. First, sending the message:
procedure SendOutput(const aThread: TMyThread; const aString: String); begin SendMessage(MSG_OUTPUT, Integer(aThread), Integer(PChar(aString))); end;
The protocol for this message is that a reference to the sending thread is passed in WParam with a pointer to the string for the event in LParam.
NOTE: This code was originally written for 32-bit Windows and I have not yet gone through it to address any potential 64-bit issues deriving from the packaging of pointers in message WParam and LParam. There may not be any or there may be plenty. I simply haven’t needed to consider it yet
These params are then unpacked and used to invoke the target event handler in the message processing method:
procedure TOutputHandler.Output(var aMessage: TMessage); var thread: TMyThread; event: TStringEvent; ptr: PChar; str: String; begin thread := TMyThread(aMessage.WParam); event := thread.OutputEvent; ptr := PChar(aMessage.LParam); str := ptr; event(str); end;
The steps here are laid out plainly so that each step can be followed clearly.
Each thread that potentially uses this handler will have an event handler that it wishes to be used. A reference to this event handler is maintained on the thread itself so the first thing that the message handler must do is extract the thread reference from the WParam, and from that obtain the target event handler.
Next, the string to be sent with the event is passed in the LParam. Or rather, a pointer to the string. So LParam is first cast as PChar and then assigned to a local String variable.
With the target event and string in hand, the message handler can then simply call the event, passing that string.
All of this occurs in the context of the VCL thread.
For an instance of TMyThread to send a string event to the assigned event handler all it has to do is call the SendOutput() method of the _OutputHandler unit variable:
procedure TMyThread.Execute; var s: String; begin while NOT Terminated do begin // ... _OutputHandler.SendOutput( self, s ); // ... end; end;
Less Is More
This is obviously not a general purpose synchronization mechanism and it is “more complicated” than simply using Synchronize() in the sense that it involves implementing a specific message handler class (which is, in essence, a message marshalling class).
But the mechanism that underpins your custom handlers is very simple. The TMessageHandler class consists of very little code, most of which simply ensures correct usage rather than “doing” anything itself. The behaviour of your callbacks is also very easily understood – callbacks are scheduled for processing just like any other message.
They arrive on and are dispatched from your application message queue just like every other (Windows message based) event that your application has to deal with.
This does create a marked difference between this mechanism and
TThread.Synchronize() exceptions raised by/escaping from the synchronized method will propagate back to the calling thread and (usually) must be handled by the thread.
Exceptions raised during the execution of event handlers invoked by my message handler mechanism propagate not to the calling thread but to the application message loop. Unless you handle them in the event handler itself, they will be deemed “unhandled” and your application default exception handler will be invoked.
However, I regard this difference as another advantage of the message based mechanism.
It makes implementing a thread-callback event no different than, say, implementing a Button.OnClick() event on a form and more often than not, this is exactly as it should be.
Next time I shall look at another threading technique, this time involving asynchronous processes of indeterminate lifetime which may expire naturally or require express termination.
Comments are now closed.