Is it better to use TThread's "Synchronize" or use Window Messages for IPC between main and child thread?

Mick picture Mick · Nov 27, 2009 · Viewed 15k times · Source

I have a rather simple multi-threaded VCL gui application written with Delphi 2007. I do some processing in multiple child threads (up to 16 concurrent) that need to update a grid control on my main form (simply posting strings to a grid). None of the child threads ever talk to each-other.

My initial design involved calling TThread's "Synchronize" to update the grid control form within the currently running thread. However, I understand that calling Synchronize essentially executes as if it is the main thread when called. With up to 16 threads running at once (and most of the child thread's processing takes from < 1 second to ~10 seconds) would Window Messages be a better design?

I've gotten it working at this point where the child thread posts a windows message (consisting of a record of several strings) and the main thread has a listener and simply updates the grid when a message is received.

Any opinions on the best method for IPC in this situation? Window messages or 'Synchronize'?

If I use window messages, do you suggest wrapping the code where I post to the grid in a TCriticalSection (enter and leave) block? Or will I not need to worry about thread safety since I'm writing to the grid in the main thread (although within the window message handler's function)?

Answer

mghie picture mghie · Nov 27, 2009

Edit:

It looks like many of the implementation details have changed since Delphi 4 and 5 (the Delphi versions I'm still using for most of my work), and Allen Bauer has commented the following:

Ever since D6, TThread doesn't use SendMessage anymore. It uses a thread-safe work queue where the "work" intended for the main thread is placed. A message is posted to the main thread to indicate that work is available and the background thread blocks on an event. When the main message loop is about to go idle, it calls "CheckSynchronize" to see if any work is waiting. If so, it processes it. Once a work item is completed, the event on which the background thread is blocked is set to indicate completion. Introduced in D2006 timeframe, TThread.Queue method was added that doesn't block.

Thanks for the correction. So take the details in the original answer with a grain of salt.

But this doesn't really affect the core points. I still maintain that the whole idea of Synchronize() is fatally flawed, and this will be obvious the moment one tries to keep several cores of a modern machine occupied. Don't "synchronize" your threads, let them work until they are finished. Try to minimize all dependencies between them. Especially when updating the GUI there is absolutely no reason to wait for this to complete. Whether Synchronize() uses SendMessage() or PostMessage(), the resulting road block is the same.


What you present here is not an alternative at all, as Synchronize() uses SendMessage() internally. So it's more of a question which weapon you want to use to shoot yourself in the foot with.

Synchronize() has been with us since the introduction of TThread in the Delphi 2 VCL, which is a shame really as it is one of the bigger design misfeatures in the VCL.

How does it work? It uses a SendMessage() call to a window that was created in the main thread, and sets the message parameters to pass the address of a parameterless object method to be called. Since Windows messages will be processed only in the thread that created the destination window and runs its message loop this will suspend the thread, handle the message in the context of the main VCL thread, call the method, and resume the thread only after the method has finished executing.

So what's wrong with it (and what's similarly wrong with using SendMessage() directly)? Several things:

  • Forcing any thread to execute code in the context of another thread forces two thread context switches, which needlessly burns CPU cycles.
  • While the VCL thread processes the message to call the synchronized method it can't process any other message.
  • When more than one thread uses this method they will all block and wait for Synchronize() or SendMessage() to return. This creates a giant bottleneck.
  • There is a deadlock waiting to happen. If the thread calls Synchronize() or SendMessage() while holding a synchronization object, and the VCL thread while processing the message needs to acquire the same synchronization object the application will lock up.
  • The same can be said of the API calls waiting for the thread handle - using WaitForSingleObject() or WaitForMultipleObjects() without some means to process messages will cause a deadlock if the thread needs these ways to "synchronize" with the other thread.

So what to use instead? Several options, I'll describe some:

  • Use PostMessage() instead of SendMessage() (or PostThreadMessage() if the two threads are both not the VCL thread). It is important though to not use any data in the message parameters that will be no longer valid when the message arrives, as the sending and receiving thread are not synchronized at all, so some other means have to be used to make sure that any string, object reference or chunk of memory are still valid when the message is processed, even though the sending thread may not even exist any more.

  • Create thread-safe data structures, put data to them from your worker threads, and consume them from the main thread. Use PostMessage() only to alert the VCL thread that new data has arrived to be processed, but don't post messages each time. If you have a continuous stream of data you could even have the VCL thread poll for data (maybe by using a timer), but this is a poor man's version only.

  • Don't use the low level tools at all, any more. If you are at least on Delphi 2007, download the OmniThreadLibrary and start to think in terms of tasks, not threads. This library has a lot of facilities for data exchange between threads and synchronization. It also has a thread pool implementation, which is a good thing - how much threads you should use does not only depend on the application but also on the hardware it's running on, so many decisions can be made at runtime only. OTL will allow you to run tasks on a thread pool thread, so the system can tune the number of concurrent threads at runtime.

Edit:

On re-reading I realize that you don't intend to use SendMessage() but PostMessage() - well, some of the above doesn't apply then, but I will leave it in place. However, there are some more points in your question I want to address:

With up to 16 threads running at once (and most of the child thread's processing takes from < 1 second to ~10 seconds) would Window Messages be a better design?

If you post a message from each thread once every second or even longer period, then the design is fine. What you should not do is post hundreds or more messages per thread per second, because the Windows message queue has a finite length and custom messages should not interfere with normal message processing too much (your program would start to appear unresponsive).

where the child thread posts a windows message (consisting of a record of several strings)

A window message can not contain a record. It carries two parameters, one of type WPARAM, the other of type LPARAM. You can only cast a pointer to such a record to one of these types, so the lifetime of the record needs to be managed somehow. If you dynamically allocate it you need to free it too, which is prone to errors. If you pass a pointer to a record on the stack or to a object field you need to make sure it is still valid when the message is processed, which is more difficult for posted messages than for sent messages.

do you suggest wrapping the code where I post to the grid in a TCriticalSection (enter and leave) block? Or will I not need to worry about thread safety since I'm writing to the grid in the main thread (although within the window message handler's function)?

There's no need to do this, as the PostMessage() call will return immediately, so no synchronization is necessary at this point. You will definitely need to worry about thread safety, unfortunately you can't know when. You have to make sure that access to data is thread-safe, by always locking the data for access, using synchronization objects. There isn't really a way to achieve that for records, the data can always be accessed directly.