14

A background-thread can be configured to receive window messages. You would post messages to the thread using PostThreadMessage. What's the correct way to exit that message loop?

Background

Before you can post messages to a background thread, the thread needs to ensure that a message queue is created by calling PeekMessage:

procedure ThreadProcedure;
var
   msg: TMsg;
begin
   //Call PeekMessage to force the system to create the message queue.
   PeekMessage(msg, NULL, WM_USER, WM_USER, PM_NOREMOVE);
end;

Now the outside world is able to post messages to our thread:

PostThreadMessage(nThreadID, WM_ReadyATractorBeam, 0, 0);

and our thread sits in a GetMessage loop:

procedure ThreadProcedure;
var
   msg: TMsg;
begin
   //Call PeekMessage to force the system to create the message queue.
   PeekMessage(msg, NULL, WM_USER, WM_USER, PM_NOREMOVE);

   //Start our message pumping loop. 
   //GetMessage will return false when it receives a WM_QUIT

   //   GetMessage can return -1 if there's an error
   //   Delphi LongBool interprets non-zero as true.
   //   If GetMessage *does* fail, then msg will not be valid. 
   //   We want some way to handle that.
   //Invalid:
   //while (GetMessage(msg, 0, 0, 0)) do
   //Better:
   while LongInt(GetMessage(msg, 0, 0, 0)) > 0 do
   begin
      case msg.message of
      WM_ReadyATractorBeam: ReadyTractorBeam;

      // No point in calling Translate/Dispatch if there's no window associated.
      // Dispatch will just throw the message away
//    else
//       TranslateMessage(Msg);
//       DispatchMessage(Msg);
//    end;
   end;
end;

My question is what's the correct way to have GetMessage receive a WM_QUIT message and return false.

We've already learned that the incorrect way to post a WM_QUIT message is to call:

PostThreadMessage(nThreadId, WM_QUIT, 0, 0);

There is even examples out there where people employ this approach. From MSDN:

Do not post the WM_QUIT message using the PostMessage function; use PostQuitMessage.

The correct way is for "someone" to call PostQuitMessage. PostQuitMessage is a special function, that sets the special flag associated with a message queue so that GetMessage will synthesize a WM_QUIT message when the time is right.

If this were a message loop associated with a "window", then the standard design pattern is when the window is being destroyed, and a WM_DESTROY message is received, we catch it and call PostQuitMessage:

procedure ThreadProcedure;
var
   msg: TMsg;
begin
   //Call PeekMessage to force the system to create the message queue.
   PeekMessage(msg, NULL, WM_USER, WM_USER, PM_NOREMOVE);

   //Start our message pumping loop. 
   //GetMessage will return false when it receives a WM_QUIT
   while Longint(GetMessage(msg, 0, 0, 0)) > 0 do
   begin
      case msg.message of
      WM_ReadyATractorBeam: ReadyTractorBeam;
      WM_DESTROY: PostQuitMessage(0);
      end;
   end;
end;

Problem is that WM_DESTROY is sent by the Window Manager. It is sent when someone calls DestroyWindow. It is wrong to just post WM_DESTROY.

Now i could synthesize some artifical WM_PleaseEndYourself message:

PostThreadMessage(nThreadID, WM_PleaseEndYourself, 0, 0);

and then handle it in my thread's message loop:

procedure ThreadProcedure;
var
   msg: TMsg;
begin
   //Call PeekMessage to force the system to create the message queue.
   PeekMessage(msg, NULL, WM_USER, WM_USER, PM_NOREMOVE);

   //Start our message pumping loop. 
   //GetMessage will return false when it receives a WM_QUIT
   while Longint(GetMessage(msg, 0, 0, 0)) > 0 do
   begin
      case msg.message of
      WM_ReadyATractorBeam: ReadyTractorBeam;
      WM_PleaseEndYourself: PostQuitMessage(0);
      end;
   end;
end;

But is there a canonical way to exit a thread's message loop?


But don't do this

It turns out that it's better for everyone that you don't use a windowless message queue. A lot of things can be unintentionally, and subtly, broken if you don't have a window for messages to be dispatched to.

Instead allocate hidden window (e.g. using Delphi's thread-unsafe AllocateHwnd) and post messages to it using plain old PostMessage:

procedure TMyThread.Execute;
var
   msg: TMsg;
begin
   Fhwnd := AllocateHwnd(WindowProc);
   if Fhwnd = 0 then Exit;
   try
      while Longint(GetMessage(msg, 0, 0, 0)) > 0 do
      begin
         TranslateMessage(msg);
         DispatchMessage(msg);
      end;
   finally
      DeallocateHwnd(Fhwnd);
      Fhwnd := 0;
   end;
end;

Where we can have a plain old window procedure to handle the messages:

WM_TerminateYourself = WM_APP + 1;

procedure TMyThread.WindowProc(var msg: TMessage);
begin
   case msg.Msg of
   WM_ReadyATractorBeam: ReadyTractorBeam;
   WM_TerminateYourself: PostQuitMessage(0);
   else
      msg.Result := DefWindowProc(Fhwnd, msg.msg, msg.wParam, msg.lParam);
   end;
end;    

and when you want the thread to finish, you tell it:

procedure TMyThread.Terminate;
begin
   PostMessage(Fhwnd, WM_TerminateYourself, 0, 0);
end;
Community
  • 1
  • 1
Ian Boyd
  • 220,884
  • 228
  • 805
  • 1,125
  • 2
    Once you have a special message, why bother with PostQuitMessage? Just get out of there. – David Heffernan May 04 '12 at 15:22
  • David, PostQuitMessage is your ticket for getting out. – Dialecticus May 04 '12 at 15:26
  • PostQuitMessage() probably does some other stuff. It seems possible that it purges the queue and forces subsequent submission attempts to fail? Not sure. – Martin James May 04 '12 at 15:35
  • 1
    What is the purpose of calling `TranslateMessage` - `DispatchMessage` if you have no window created in a thread? – kludg May 04 '12 at 15:52
  • Use Delphi `Break` operator to exit thread message loop, like any other loop. – kludg May 04 '12 at 15:55
  • 1
    @Dialectus break would be easier! – David Heffernan May 04 '12 at 16:01
  • 1
    @Dialecticus, you don't need a ticket to get out. You're free to leave whenever you want. – Rob Kennedy May 04 '12 at 16:11
  • 4
    Unlike the Hotel California! ;-) – David Heffernan May 04 '12 at 16:12
  • @Martin, it doesn't do anything else. It definitely doesn't purge the queue. All it does is set a flag, so the next time you call `GetMessage` and there aren't any other messages, it will return `wm_Quit`. – Rob Kennedy May 04 '12 at 16:12
  • 1
    I think the PeekMessage call is superfluos. – Sertac Akyuz May 04 '12 at 16:51
  • 2
    @Sertac Correct, the [documentation](http://msdn.microsoft.com/en-us/library/windows/desktop/ms644928(v=vs.85).aspx) makes it clear that calling `GetMessage` creates the message queue. – David Heffernan May 04 '12 at 16:54
  • @David - Thanks, I looked for it to be certain but couldn't find.. – Sertac Akyuz May 04 '12 at 16:59
  • @Serg i *thought* the purpose of calling `DispatchMessage` would be to ensure that i'm processing messages, so that COM still functions from within my thread. i don't know exactly *why* COM needs my thread to keep processing messages, or *how* COM works if i don't "*dispatch messages that aren't mine to the Windows ether*", but looks like calling `DispatchMessage` in a window-less message pump serves no purpose. i'll leave the code in, but commented out, so future me knows that calling `DispatchMessage` is wrong. – Ian Boyd May 04 '12 at 17:18
  • DispatchMessage is indeed needed if you're using COM. COM creates a hidden window for itself and sends messages there when marshaling cross-apartment calls to ensure methods are executed in the right thread. – Rob Kennedy May 04 '12 at 18:02
  • You really need a thread window if you have 2 or more message loops in a thread, and as in oldnewthing article you don't control some loops; in this case a windowless message will be discarded if processed in wrong message loop, while a message with destination hWnd will be dispatched to the window procedure by `DispatchMessage`. – kludg May 04 '12 at 18:07
  • @RobKennedy - OK, thanks:) Luckily, I rarely use COM and so don't need to use Windows Message Queues for signalling to work threads. I can't recollect ever calling PostQuitMessage() explicitly. – Martin James May 04 '12 at 18:53
  • DDE is one example where you might have a windowless app that still requires a message loop. If it's written as a console app you need a way to pass the Ctrl+C signal onto the main thread. Yes, it's an edge case, but it's still a valid one :-). – Chris Oldwood Feb 13 '13 at 17:36

2 Answers2

7

There is no canonical way; there is no canon. You get GetMessage to return zero by posting a wm_Quit message, and you do that by calling PostQuitMessage. When and how you know to do that is up to you.

It common to do it in response to wm_Destroy, but that's only because the common way to exit programs is by closing windows. If you have no window to close, then choose a different way. Your wm_PleaseEndYourself idea is fine.

You don't even have to send a message. You could use some waitable object like an event or a semaphore, and then use MsgWaitForMultipleObjects to detect whether it's signaled while also waiting for new messages.

You don't really even have to wait for GetMessage to return zero. If you already know the thread needs to stop, then you can simply stop processing messages entirely. There are lots of ways to exit a loop. You could use exit, break, raise, or even goto, or you could set a flag that you check in the loop condition along with the return value of GetMessage.

Note also that GetMessage returns -1 on failure, which as a non-zero value will be interpreted as true. You probably don't want to continue your message loop if GetMessage fails, so instead check for GetMessage(...) > 0, or do like the documentation recommends.

Rob Kennedy
  • 156,531
  • 20
  • 258
  • 446
  • Ahhh, i was confused because [`GetMessage`](http://msdn.microsoft.com/en-us/library/windows/desktop/ms644936(v=vs.85).aspx) returns a `BOOL`. i'll update the question with a note about the *wrong* way to do it. – Ian Boyd May 04 '12 at 17:07
6

Using PostThreadMessage is not necessarily incorrect. Raymond's article that you linked to says:

Because the system tries not to inject a WM_QUIT message at a "bad time"; instead it waits for things to "settle down" before generating the WM_QUIT message, thereby reducing the chances that the program might be in the middle of a multi-step procedure triggered by a sequence of posted messages.

If the concerns outlined here do not apply to your message queue, then call PostThreadMessage with WM_QUIT and knock yourself out. Otherwise you'll need to create a special signal, i.e. a user-defined message, that allows you to call PostQuitMessage from the thread.

David Heffernan
  • 572,264
  • 40
  • 974
  • 1,389