How to exit a thread's message loop?

delphi, message-loop, multithreading, winapi

Solution

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.

Problem

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; ```

Original source

Related problems