iCodeSometime
iCodeSometime

Reputation: 1652

How to keep a thread's message pump reactive

I am implementing a thread that needs the following features:

  1. Promptly responds to termination requests
  2. Pumps messages
  3. Remains responsive to SendMessage requests while waiting for a message

My initial implementation of the message pump used GetMessage like:

while not Terminated and GetMessage(Msg, 0, 0, 0) do
begin
  TranslateMessage(Msg);
  DispatchMessage(Msg);
end;

The problem I found with that, is that GetMessage will never return unless there is a message. Meaning, if there is low message activity, it may be quite a while before it checks Terminated again.

My second implementation (inspired by this answer) used MsgWaitForMultipleObjects to wait until a message exists before checking (since it has a timeout)

while not Terminated do
begin
  if MsgWaitForMultipleObjects(0, nil^, False, 1000, QS_ALLEVENTS) = WAIT_OBJECT_0 then
  begin
    while PeekMessage(Msg, 0, 0, 0, PM_REMOVE) do
    begin
      TranslateMessage(Msg);
      DispatchMessage(Msg);
    end;
  end;
end;

The problem I've found with this, is that MsgWaitForMultipleObjects blocks the thread while it waits. So, when a message is sent to the thread via SendMessageTimeout, it times out, where it doesn't when using GetMessage.

The solution that comes to mind is to go back to the GetMessage implementation, but add a timer to make sure a WM_TIMER message resets the loop every second.

Is this really the only way to do this? It seems there should be some better way to keep the thread responsive while waiting for messages.

Upvotes: 3

Views: 470

Answers (1)

Remy Lebeau
Remy Lebeau

Reputation: 598134

My initial implementation of the message pump used GetMessage like:

while not Terminated and GetMessage(Msg, 0, 0, 0) do
begin
  TranslateMessage(Msg);
  DispatchMessage(Msg);
end;

The problem I found with that, is that GetMessage will never return unless there is a message. Meaning, if there is low message activity, it may be quite a while before it checks Terminated again.

You can override the thread's virtual TerminatedSet() method to post a message to the queue via PostMessage() or PostThreadMessage() to "wake up" GetMessage() if it is blocked.

Alternatively, have your thread constructor create a TEvent object, and free it in the thread's destructor. Then have TerminatedSet() signal that event. Your loop can then use MsgWaitForMultipleObjects() to wait on the message queue AND the event at the same time. The return value will tell you whether the wait was satisfied by a message or the event.

My second implementation (inspired by this answer) used MsgWaitForMultipleObjects to wait until a message exists before checking (since it has a timeout)

while not Terminated do
begin
  if MsgWaitForMultipleObjects(0, nil^, False, 1000, QS_ALLEVENTS) = WAIT_OBJECT_0 then
  begin
    while PeekMessage(Msg, 0, 0, 0, PM_REMOVE) do
    begin
      TranslateMessage(Msg);
      DispatchMessage(Msg);
    end;
  end;
end;

The problem I've found with this, is that MsgWaitForMultipleObjects blocks the thread while it waits. So, when a message is sent to the thread via SendMessageTimeout, it times out, where it doesn't when using GetMessage.

The SendMessage...() family of functions will deliver a message directly to the target window's message procedure, bypassing the message queue completely. So MsgWaitForMultipleObjects() and (Get|Peek)Message() will never report a sent message from SendMessage...(), only a posted message from PostMessage() or PostThreadMessage() (or a synthesized message, like WM_TIMER, WM_PAINT, etc). However, when sending a message across thread boundaries, the receiving thread still needs to perform message retrieval calls (is, (Get|Peek)Message()) in order for the sent message to actually be delivered to the window procedure.

The solution that comes to mind is to go back to the GetMessage implementation, but add a timer to make sure a WM_TIMER message resets the loop every second.

Inside a thread, it would be better to use a waitable timer instead of WM_TIMER, then you can use the timer with MsgWaitForMultipleObjects(). But really, there is very little difference between using GetMessage() with WM_TIMER vs MsgWaitForMultipleObjects() with a timeout, so there is no need to waste system resources creating the timer.

Upvotes: 5

Related Questions