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 Synchronize()
.
With 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.
Coming Next…
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.
Well, what about using the OmniThreadLibrary? Threading is much simpler with it…
I’ve simply never felt the need to turn to such libraries. ymmv. <shrug> ๐
As you noted the actual implementation is over-engineered but in the past it was based on sending a message to the main thread.
The change happened from D5 to D6 where Kylix was introduced. The rationale was to make it cross-platfom. I known it because iIhad a bug using TThread switching from BCB5 To BCB6 and then I browsed the source code….
Regards,
Stefano Moratto
Thanks for the historical note. ๐
I don’t have D5 on the machine I am at right now but I will be interested to go back and look at the Synchronize implementation pre-D6. My memory is that it was still a relatively complex implementation, perhaps due to the way exceptions were propagated back to the “calling thread” ?
I don’t remember the exception propagation. However I can tell you that checksynchronize method has been introduced with the new implementation. The application process message method calls it in order to check new synchronized call. Before this was handled by a message handler.
My problem was I called synchronize in an activex written in bcb. Checkforsynchronize is not called because the application message pump is implemented by the activex host, not by TApplication!
At the end I wrote my own implementation of synchronize. it used a custom message handler. I used an helper queue to hold information about the method to call and its parameters. So I added an Item to this queue in a secondary thread, I posted a message to the main thread. When it received this custom message it dequeued the item and called it using the parameters. This queue was implemented by a circular buffer, this allowed to not do memory allocation to pass parameters.
To make the code work for 64bit, all you have to do is. Change the typecasts on the SendMessage() call to use WPARAM() and LPARAM() instead of Integer(). As for “passing a pointer to a string”, you are not. You are passing a pointer to the string’s payload and then allocating a new string when preparing to call the target event. To truely pass a pointer to the source string, use @aString instead of PChar(aString), and PString(Message.LParam)^ instead of PChar(Message.LParam).
Thanks for the LPARAM/WPARAM tip. ๐
Re the strings, yes you’re quite right I over simplified the terminology somewhat, but the allocation of a string specifically for passing via the event was deliberate caution, creating a string for the event distinct from the source (though perhaps unnecessary given the synchronous nature of the communications). There may be scope for an optimisation here using the incantations you suggested. Thanks. ๐
Messages are also quite handy for automatically starting some action depending on the program’s parameters, but only after the main form has finished initializing and is showing. I have seen this done by other means, like calling Application.ProcessMessages or misusing the Form_Show or Form_Activate events, but none is as clean as posting a custom message from the constructor that gets handled after all messages already in the message queue have been handled.
Yep, messages are very useful in many situations where event serialisation is useful.
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’
You’re being overly dramatic – if the code is hard to read it’s because it adopts an inline IFDEF approach, not because the TThread interface is particularly unsuited to *nix.
You know of whom I speak
Funnily enough, the FMX platform-specific code is much easier to read compared to the RTL’s because it doesn’t (in the main) use inline IFDEFs, and instead puts platform specific bits in separate units.
Um, I never said it was “unsuited for *nix”. And what I did say wasn’t “dramatic” at all, overly or otherwise. Just a statement of the history of the class. I myself made the point that the “complexity” derived from the impaired readability. But thank you for clarifying that this readability impairment is the result of $ifdef‘s, though I thought that this would have been obvious to anyone interested enough to look at the source themselves and didn’t think it necessary to mention how it was less readable and thus more complex to follow.
If I was being “overly dramatic” then you I think are being overly defensive. I made no comment or observation about quality of FMX in this post.
I merely observed that the complexity of the thread class stems from the need to satisfy “legacy” VCL expectations as well as supporting other platforms. As Stefan has mentioned, one of those platforms was introduced by Kylix, long before and entirely unrelated to FMX.
But today those other platforms are only required by FMX, hence that is the only “other guy“, since the other “other guy” left the party some time ago.
People think I “attack FireMonkey” unrelentingly. I think perhaps the problem is that some people feel the need to defend it constantly, which in their mind amounts to the same thing but isn’t actually the case. ๐
There is a WM_COPY or something like this. Windows manages memory transfer in this case. Unfortunately, PostMessage is not possible for WM_COPY.
Given the raising popularity of node.js and alike it would be nice to migrate to non-Windows main loop. E. g. libuv so that Delphi callbacks could be shared with node.js callbacks, node.js extensions callbacks and Delphi DLLs could be node.js extensions.
Thanks for the post!