How to exit a thread's message loop?
Asked Answered
S

2

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;
Spacial answered 4/5, 2012 at 15:11 Comment(17)
Once you have a special message, why bother with PostQuitMessage? Just get out of there.Twinscrew
David, PostQuitMessage is your ticket for getting out.Presley
PostQuitMessage() probably does some other stuff. It seems possible that it purges the queue and forces subsequent submission attempts to fail? Not sure.Trappings
What is the purpose of calling TranslateMessage - DispatchMessage if you have no window created in a thread?Shutter
Use Delphi Break operator to exit thread message loop, like any other loop.Shutter
@Dialectus break would be easier!Twinscrew
@Dialecticus, you don't need a ticket to get out. You're free to leave whenever you want.Robi
Unlike the Hotel California! ;-)Twinscrew
@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.Robi
I think the PeekMessage call is superfluos.Shortwave
@Sertac Correct, the documentation makes it clear that calling GetMessage creates the message queue.Twinscrew
@David - Thanks, I looked for it to be certain but couldn't find..Shortwave
@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.Spacial
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.Robi
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.Shutter
@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.Trappings
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 :-).Beardless
R
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.

Robi answered 4/5, 2012 at 16:10 Comment(1)
Ahhh, i was confused because GetMessage returns a BOOL. i'll update the question with a note about the wrong way to do it.Spacial
T
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.

Twinscrew answered 4/5, 2012 at 16:11 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.