3

I am working on small monitoring application which will have some threads for communication with some devices via SNMP, TCP, ICMP, other threads have to perform some calculations. All this result I have to output in GUI (some Forms or TabSheets).

I am thinking about next possibilities:

  • use Synchronize from every worker thread:
  • use shared buffer and windows messaging mechanism. Thread will put message in shared buffer (queue) and will notify GUI with windows message.
  • use separate thread which will listen for Synchronization primitives (Events, Semaphores, etc) and use again Synchronize, but only from GUI-dedicated thread only, or Critical Section on GUI to display message.
  • UPDATE: (Proposed by one workmate) use shared buffer and TTimer in main form which will check periodically (100-1000 ms) shared buffer and consuming, instead of windows messaging. (Does it have some benefit over messaging?)
  • Other?

Dear experts, please explain what is the best practice or what are the advantages and disadvantages of exposed alternatives.

UPDATE:
As idea:
//shared buffer + send message variant
LogEvent global function will be called from everywhere (from worker threads too):

procedure LogEvent(S: String);
var
  liEvent: IEventMsg;
begin
  liEvent := TEventMsg.Create; //Interfaced object
  with liEvent do
  begin
    Severity := llDebug;
    EventType := 'General';
    Source := 'Application';
    Description := S;
  end;
  MainForm.AddEvent(liEvent); //Invoke main form directly
end;

In Main Form, where Events ListView and shared section (fEventList: TTInterfaceList which is already thread-safe) we'll be:

procedure TMainForm.AddEvent(aEvt: IEventMsg);
begin
  fEventList.Add(aEvt);
  PostMessage(Self.Handle, WM_EVENT_ADDED, 0, 0);
end;

Message handler:

procedure WMEventAdded(var Message: TMessage); message WM_EVENT_ADDED;
...
procedure TMainForm.WMEventAdded(var Message: TMessage);
var
  liEvt: IEventMsg;
  ListItem: TListItem;
begin
  fEventList.Lock;
  try
    while fEventList.Count > 0 do
    begin
      liEvt := IEventMsg(fEventList.First);
      fEventList.Delete(0);
      with lvEvents do //TListView
      begin
        ListItem := Items.Add;
        ListItem.Caption := SeverityNames[liEvt.Severity];
        ListItem.SubItems.Add(DateTimeToStr(now));
        ListItem.SubItems.Add(liEvt.EventType);
        ListItem.SubItems.Add(liEvt.Source);
        ListItem.SubItems.Add(liEvt.Description);
      end;
    end;
  finally
    fEventList.UnLock;
  end;
end;

Is there something bad? Main Form is allocated ONCE on application startup and Destroyed on application exit.

ALZ
  • 1,997
  • 2
  • 27
  • 44
  • They all seem much the same. Which is better? Who knows. We cannot really tell since we don't know all the constraints. Does performance matter? What is driving your design? – David Heffernan Dec 30 '13 at 00:04
  • I usually PostMessage request/result objects directly in wParam/lParam, either freeing them or repooling them in the message-handler after processing the object. There is indeed some sort of 'movement' to use a separate thread-safe queue to actually transfer the data and use the Window message as a kind of flag to pop the queue, but I've never understood that approach - why use two queues when one will do? – Martin James Dec 30 '13 at 00:30
  • @DavidHeffernan, it is Event-Driven. Event is SNMP reply from device. Performance matters, because SNMP polling periodicity may be 0.5-5 seconds. (I hope I expressed myself properly :)) – ALZ Dec 30 '13 at 06:07
  • @MartinJames, I read somewhere that some windows messages may be lost, for this reason some guys recommend, for example, to put Data in own thread-safe Queue (`TThreadedQueue`) once, then `PostMessage`. On receiving side, on message handling try process all messages in Q at once. Actually this is my option – ALZ Dec 30 '13 at 06:11
  • 1
    @TLama: Of course messages can be lost, the message queue has a finite length so posting messages faster than they are processed will at one time lead to the queue being full and the messages being "lost" unless the sender correctly handles the error result of `PostMessage()`. See http://stackoverflow.com/questions/123323/how-deep-is-the-win32-message-queue – mghie Dec 30 '13 at 09:53
  • Once upon a time I wrote a Delphi HelloWorld app that send to Windows 1 million messages then my USER message. My messages didn't pass to the window :) – ALZ Dec 30 '13 at 09:57
  • Please do not edit your question so that it becomes a different question. – kludg Dec 30 '13 at 13:37
  • @user246408, Ok, got it! But I just added some more details and didn't change idea. If it's necessary I can remove updates. – ALZ Dec 30 '13 at 13:43
  • 1
    Timer is not needed; you better use Application.OnIdle to check the buffer. – kludg Dec 30 '13 at 13:52
  • 1
    Btw. if you were going to consume the whole queue in your `WMEventAdded` method (which seems you did), don't forget to lock updates for that list view (`Items.BeginUpdate`, `Items.EndUpdate`). That will lock the control for painting, so the list view won't repaint itself every time you add an item when the system asks for it. – TLama Dec 30 '13 at 14:50
  • Maybe i'd tried n+1 threads. N worker threads would post to the aggregating thread the pointers to their filled buffers and then would allocate new buffers. The aggregator thread would copy the data into uniform common buffer, free those thread-local buffers and raise semaphore for main form that an aggregate buffer has unread data. Main form would when appropriate read the aggregate buffer in uniform `while idx < Count-1` manner and clean the "unread data" semaphore after setting "last read item index". The tricky thing would be to flush (re-submit) data that arrived why form was in-reading – Arioch 'The Dec 31 '13 at 17:16
  • ...perhaps by timeouts if worker threads sent no new data after XXX msecs and when the service would be stopped. – Arioch 'The Dec 31 '13 at 17:17
  • 2
    @ALZ _"Once upon a time I wrote a Delphi HelloWorld app..."_ This is why you should always check the return value of `PostMessage()`. Also, the easiest way to fill up a message queue is to call `PostMessage()` within a loop from the same thread you're posting to. Because nothing is removing queue entries while you're posting. So in general you want to be wary of threads posting large numbers of messages to themselves. – Disillusioned Jan 01 '14 at 15:42

2 Answers2

6

Use Synchronize from every worker thread

This would probably be the simplest approach to implement, but as others have indicated will lead to your IO threads being blocked. This may/may not be a problem in your particular application.

However it should be noted that there are other reasons to avoid blocking. Blocking can make performance profiling a little trickier because it effectively pushes up the time spent in routines that are "hurrying up and waiting".

Use shared buffer and windows messaging mechanism

This is a good approach with a few special considerations.

If your data is extremely small, PostMessage can pack it all into the parameters of the message making it ideal.

However, since you make mention of a shared buffer, it seems you might have a bit more data. This is where you have to be a little careful. Using a "shared buffer" in the intuitive sense can expose you to race conditions (but I'll cover this in more detail later).

The better approach is to create a message object and pass ownership of the object to the GUI.

  • Create a new object containing all the details required for the GUI to update.
  • Pass the reference to this object via the additional parameters in PostMessage.
  • When the GUI finishes processing the message it is responsible for destroying it.
  • This neatly avoids the race conditions.
  • WARNING: You need to be certain the GUI gets all your messages, otherwise you will have memory leaks. You must check the return value of PostMessage to confirm it was actually sent, and you may as well destroy the object if not sent.
  • This approach works quite well if the data can be sent in light-weight objects.

Use separate thread ...

Using any kind of separate intermediate thread still requires similar considerations for getting the relevant data to the new thread - which then still has to be passed to the GUI in some way. This would probably only make sense if your application needs to perform aggreagation and time-consuming calculations before updating the GUI. In the same way that you don't want to block your IO threads, you don't want to block your GUI thread.

Use shared buffer and TTimer in main form

I mentioned earlier that the "intuitive idea" of a shared buffer, meaning: "different threads reading and writing at the same time"; exposes you to the risk of race conditions. If in the middle of a write operation you start reading data, then you risk reading data in an inconsistent state. These problems can be a nightmare to debug.

In order to avoid these race conditions you need to fall back on other synchronisation tools such as locks to protect the shared data. Locks of course bring us back to the blocking issues, albeit in a slightly better form. This is because you can control the granularity of the protection desired.

This does have some benefits over messaging:

  • If your data structures are large and complex, your messages might be inefficient.
  • You won't need to define a rigorous messaging protocol to cover all update scenarios.
  • The messaging approach can lead to a duplication of data within the system because the GUI keeps its own copy of the data to avoid race conditions.

There is a way to improve the idea of shared data, only if applicable: Some situations give you the option of using immutable data structures. That is: data structures that do not change after they've been created. (NOTE: The message objects mentioned earlier should be immutable.) The benefit of this is that you can safely read the data (from any number of threads) without any synchronisation primitives - provided you can guarantee the data doesn't change.

Community
  • 1
  • 1
Disillusioned
  • 14,635
  • 3
  • 43
  • 77
  • 1
    Pass the reference to this object via the additional parameters in PostMessage is an open door to unexpected memory leaks, IMHO - a thread-safe queue is IMHO much safer: in this case, you just send a notification message, and all pending information is retrieved from the queue, even by-passing the deprecated values, if needed. Locking is not a big issue if access does not take a lot of time - and instead of *immutable* structures, you can just use a private copy or unqueue the information for further processing, then release the lock. – Arnaud Bouchez Jan 03 '14 at 08:32
0

The best approach is to use a GDI custom message and just call PostMessage() to notify the GUI.

type
  TMyForm = class(TForm)
  .
  .
  .
  private
    procedure OnMyMessage(var Msg: TMessage); message WM_MY_MESSAGE;
    procedure OnAnoMessage(var Msg: TMessage); message WM_ANO_MESSAGE;
  .
  .


  PostMessage(self.Handle,WM_MY_MESSAGE,0,0);

See this great article for full explanation.

This is a lighter/faster approach to rely on the OS internal features.

Arnaud Bouchez
  • 42,305
  • 3
  • 71
  • 159
  • 5
    Be aware that the `TWinControl.Handle` property getter is NOT thread-safe! You should either 1) post to `TApplication.Handle` and use the `TApplication(Events).OnMessage` event to retrieve it, or 2) post to a dedicated window created by `AllocateHWnd()`. – Remy Lebeau Dec 30 '13 at 09:31
  • 2
    How can you be sure this is the best? – David Heffernan Dec 30 '13 at 09:52
  • @RemyLebeau AFAIK PostMessage() will queue in the background thread, but unqueue will be done in the MAIN GUI thread (in the main Forms.ProcessMessages loop). So it is perfectly thread-safe to use PostMessage + WM_USER. Having the TForm.Handle available and not in a destroying state is IMHO a decent prerequisite: and even if you are destroying the TForm, PostMessage(Form.Handle) will just be a no-op, since all TForm message process will take place in the main GUI thread. – Arnaud Bouchez Dec 30 '13 at 11:20
  • 1
    @DavidHeffernan OP asked for an opinion. This is just mine, from years of feedback of using this approach. As I wrote, it is IMHO the best approach, since it "is a lighter/faster approach to rely on the OS internal features", whereas any other TThread/Synchronize approach is a blocking process, which can be error-prone. – Arnaud Bouchez Dec 30 '13 at 11:21
  • 2
    @ArnaudBouchez the issue is window recreation. AllocateHWnd is the right way here. – David Heffernan Dec 30 '13 at 11:24
  • Guys, some details: Message Handler is on main form, which is single form of application. I added some code snippet and one more received proposal. – ALZ Dec 30 '13 at 13:22
  • 1
    @ALZ You need a window handle that is not subject to re-creation. `AllocateHWnd` is what you are looking for. – David Heffernan Dec 30 '13 at 14:41
  • "As I wrote, it is IMHO the best approach". No, you wrote, "The best approach is ...". I just wanted to know **why** you feel that this approach is best for the asker's problem. – David Heffernan Dec 30 '13 at 14:42
  • 1
    @ArnaudBouchez: `PostMessage()` is thread safe, but the `TWinControl.Handle` property getter is NOT thread safe. The `TWinControl` HWND is not persistent, it can (and does) get recreated dynamically during the control's lifetime. Bad things can happen when recreation occurs on an HWND being used by threads, including lost messages, crashes, handle leaks, and a dead control that no longer responds to the main thread. Technical details have been posted in past discussions. That is why you should use a persistent, preferably dedicated, HWND to post to instead. – Remy Lebeau Dec 30 '13 at 17:53
  • @RemyLebeau, some links? please! – ALZ Dec 30 '13 at 21:31
  • 1
    You don't need any links. It's all here. Use AllocateHWnd. End of story. – David Heffernan Dec 30 '13 at 23:55
  • @ALZ: There have been TONS of discussions in the Embarcadero forums over the years on this very matter. I am not going to spend my time digging up old discussion links. Please do your own searches. – Remy Lebeau Dec 30 '13 at 23:59
  • 1
    Already used. If somebody of you will post such response + code snippet, then I'll accept it, otherwise I'll put my own answer after some tests already next year. HAPPY NEW YEAR, GUYS! :) – ALZ Dec 31 '13 at 06:10
  • That cannot be most light-weight solution, as VCL does a lot of ping-pong with messages, faking flags, adding layers of custom handling and GetDynaMethod attempts and all that. To do it light-weight a special dedicated object with streamlined message loop is preferred. Like maybe in OTL – Arioch 'The Dec 31 '13 at 16:19
  • @RemyLebeau I agree that `TWinControl` HWND may be re-created, but it is not the case of a `TForm`, once it has been displayed on screen, unless you use a very specific content. For simple UI process, it is working very well in practice. See [this answer if you need a thread-safe AllocateHWnd`](http://stackoverflow.com/a/8820298/458259). @Arioch'The GDI messages are always lighter than a blocking solution based on `Synchronize`, as I stated in my answer. – Arnaud Bouchez Jan 03 '14 at 08:26
  • A TForm HWND is not 100% persistent. Like any other TWinControl, its HWND **can** be recreated dynamically. Best not to rely on it. AllocateHWnd() is safer. If you want something even more lightweight, use CreateWindow/Ex() directly instead. – Remy Lebeau Jan 04 '14 at 09:46
  • @RemyLebeau, @David Heffernan Thanks a lot, I never supposed that TForm.Handle may be recreated, now I am using `AllocateHWnd`, but response I'll accept after I'll perform some tests with other alternatives. – ALZ Jan 06 '14 at 08:20