Things you shouldn’t do, Part 4: MsgWaitForMultipleObjects is a very tricky API

When you’re writing a windows application, there are often times that you need to signal your UI thread that an event has occurred. One of the ways to do this of course is to post a message to a window on the UI thread.

But sometimes using a window message isn’t convenient. For example, if you’re writing to a file asynchronously, you could create an event, stick it in an LPOVERLAPPED structure, and call WriteFile asynchronously. When the write completes, the event’s set to the signaled state.

In this case, it’s often convenient to put a call to MsgWaitForMultipleObjects in your message loop – this routine will block until either a message is posted to the thread (qualified by the dwWakeMask parameter to MsgWaitForSingleObjects), or until the events are signaled.

The thing that makes this API “tricky” is the bWaitAll parameter. The bWaitAll parameter is documented the same as the bWaitAll parameter for WaitForMultipleObjects’ bWaitAll parameter:

[in] If this parameter is TRUE, the function returns when the states of all objects in the pHandles array have been set to signaled and an input event has been received. If this parameter is FALSE, the function returns when the state of any one of the objects is set to signaled or an input event has been received. In this case, the return value indicates the object whose state caused the function to return.

But this isn’t quite the case. For example, if you were only waiting on a single event (as in the example above), you might be tempted to set bWaitAll to TRUE. After all, since there’s only one object being waited on, all of the objects will be signaled when the one event is set to the signaled state, right?

Actually no. In this case, MsgWaitForMultipleObjects will block until both the event is set to the signaled state, AND a message that meets the dwWaitMask criteria is posted to the thread. The reason for this is simple (but subtle). A hint as to the reason can be found in the documentation for the nCount parameter:

[in] Number of object handles in the array pointed to by pHandles. The maximum number of object handles is MAXIMUM_WAIT_OBJECTS minus one.

Why on earth is it MAXIMUM_WAIT_OBJECTS minus one? Well, it’s because under the covers, the MsgWaitForSingleObjects takes your wait array, and adds an additional handle that’s associated with the thread. It then forwards the call to WaitForMultipleObjectsEx. This additional handle is set to the signaled state when a message that meets the dwMaskCriteria is queued to the threads message queue. Since the bWaitAll parameter is simply passed onto WaitForMultipleObjectsEx, all of the events have to be set to the signaled state, including the event handle that was added to the wait list.

So the bottom line is: you need to be very, very careful when calling MsgWaitForMultipleObjects – if you specify bWaitAll as true, you may find that your application doesn’t wake up when you expected it to.

I’ll admit to subverting this API to get a version of GetMessage with a timeout. You have to supply at least one handle, though, at least on Pocket PC (which doesn’t have the problem you describe, because WaitAll isn’t supported).

So, it would seem that the other way this functionality could have been in the API is if there was a GetThreadMsgHandleFoo(hthread, dwFlags, pEventHandle) function that returned the handle. MsgWaitForMultipleObjects requires mystical divination on the part of the implementation to figure out when that handle needs to be created and closed.

Raymond: Not necessarily. The event could be created in kernel mode, with the only access granted to user mode being SYNCHRONIZE and not EVENT_MODIFY_STATE. Then you could only wait on it.

Besides, the messaging subsystem already has to be robust enough to handle this. MsgWaitForMultipleObjects does get a handle internally (long ago I looked at the implementation and even wrote a program to call the internal function to grab the message queue event handle). So a meddling application could already cause this problem if it really wanted to.