Parallel Programming with C++ – Part 2 – Asynchronous Procedure Calls and Window Messages

In part 1 of the Parallel Programming with C++ series I introduced asynchronous procedure calls (APCs) and how they can be used with alertable I/O to process asynchronous I/O requests without blocking an application’s thread.

Of course the example still ended up blocking since the SleepEx function was used to flush the APC queue. Fortunately that’s not the only function that Windows provides to place a thread in an alertable state and in fact Windows provides a number of such functions with different characteristics. One that is particularly useful for client applications is MsgWaitForMultipleObjectsEx as it allows you to integrate APC handling into a thread’s message loop.

Before we can examine this function however I first need to recap how message loops work. Any thread that creates windows directly or indirectly must remove and dispatch messages destined for its windows. Normally you shouldn’t worry about writing your own message loop. Whatever user interface framework you happen to be using, whether its MFC, WTL, Windows Forms or even WPF, will provide an implementation tailored for it. At the heart of any message loop however are a few fundamental functions. The GetMessage function (usually) removes a message from a thread’s message queue and copies the message information to the provided MSG structure. If the message queue is empty, GetMessage will wait until one arrives in the queue before returning. A return value of -1 indicates that an error occurred retrieving the message. Usually this is handled simply by looping again and waiting for the next message. A return value of 0 indicates that the WM_QUIT message was retrieved. This is usually a signal that the message loop should exit and the thread should terminate. Any other return value indicates that a message was successfully retrieved and should be dispatched to the appropriate window procedure for handling. Here is a simple message loop implementation:

int Run()
{
    MSG messageInfo = { 0 };

    while (true)
    {
        // Wait for next message.
        BOOL result = ::GetMessage(&messageInfo,
                                   0, // all windows
                                   0, // all messages
                                   0); // all messages

        if (-1 == result)
        {
            TRACE(L"GetMessage failed (%d)\n", ::GetLastError());
            continue;
        }

        if (0 == result)
        {
            ASSERT(WM_QUIT == messageInfo.message);
            break;
        }

        // Send message to window procedure.
        ::DispatchMessage(&messageInfo);
    }

    // Return the WM_QUIT exit code.
    return static_cast<int>(messageInfo.wParam);
}

Please keep in mind that message loops in practice can be a lot more complicated. You might want to translate keyboard input into character messages, allow a window to “pre-translate” a message for dialog handling, etc. but the example above is sufficient for this discussion.

Now back to APCs. In part 1 I mentioned that you can use SleepEx with an interval of zero to handle all pending APCs and then return immediately. One (flawed) solution is to integrate SleepEx with the message loop above. The problem is that it would only handle APCs when messages arrive since queued APCs will not signal GetMessage to return. What we need is a function that will wait on both a thread’s message queue and its APC queue. Fortunately just such a function exists and as I mentioned before it is called MsgWaitForMultipleObjectsEx. Like SleepEx it places a thread into an alertable state so that APCs can be handled, but unlike GetMessage it does not retrieve messages from a thread’s message queue but simply returns indicating that messages are available. Fortunately that’s all we need to update our message loop to also handle APCs efficiently.

Since MsgWaitForMultipleObjectsEx is doing the waiting for us, we cannot also use GetMessage to retrieve messages from the queue and instead need to use the closely related PeekMessage function. PeekMessage is similar to GetMessage but it does not wait for a message. It will optionally remove a message from the queue but if a message is not available it will return immediately. Fortunately that’s exactly the behaviour we need. We can simply call PeekMessage in a loop to flush the message queue and then call MsgWaitForMultipleObjectsEx to wait for new messages or APCs. Here’s an updated message loop reflecting this:

int Run()
{
    MSG messageInfo = { 0 };

    while (WM_QUIT != messageInfo.message)
    {
        DWORD result = ::MsgWaitForMultipleObjectsEx(0, // no handles
                                                     0, // no handles
                                                     INFINITE,
                                                     QS_ALLINPUT,
                                                     MWMO_ALERTABLE | MWMO_INPUTAVAILABLE);

        if (WAIT_FAILED == result)
        {
            TRACE(L"MsgWaitForMultipleObjectsEx failed (%d)\n", ::GetLastError());
            continue;
        }

        ASSERT(WAIT_IO_COMPLETION == result || WAIT_OBJECT_0 == result);

        if (WAIT_OBJECT_0 == result)
        {
            while (::PeekMessage(&messageInfo,
                                 0, // any window
                                 0, // all messages
                                 0, // all messages
                                 PM_REMOVE))
            {
                if (WM_QUIT == messageInfo.message)
                {
                    break; // WM_QUIT retrieved so stop looping
                }

                ::DispatchMessage(&messageInfo);
            }
        }
    }

    ASSERT(WM_QUIT == messageInfo.message);
    return static_cast<int>(messageInfo.wParam);
}

As you might have guessed, MsgWaitForMultipleObjectsEx is an alertable version of MsgWaitForMultipleObjects and both are similar to WaitForMultipleObjects(Ex) in that they can wait for kernel objects to be signalled. In this case however we don’t need to wait on kernel objects so the first and second parameters are set to zero. The third parameter indicates the minimum number of milliseconds that the thread should be suspended (unless the wake conditions are met). INFINITE indicates that the call should not time out. The second-to-last parameter is a bitmask indicating the types of messages that will force the call to return. QS_ALLINPUT indicates that the call should return as soon as any message is queued. You can tailor this to only wait for certain messages such as mouse input or paint messages. The last parameter indicates that the call should return if APCs are queued and handled (MWMO_ALERTABLE) or if messages were previously queued (MWMO_INPUTAVAILABLE). The latter is needed since MsgWaitForMultipleObjectsEx may not otherwise return if messages are queued prior to calling MsgWaitForMultipleObjectsEx.

MsgWaitForMultipleObjectsEx returns WAIT_FAILED if it fails for some reason. Call the GetLastError function for the actual reason. It returns WAIT_IO_COMPLETION to indicate that one or more APCs were handled. And it returns WAIT_OBJECT_0 if messages are waiting in the queue in which case they are de-queued using PeekMessage with the PM_REMOVE flag and dispatched to the appropriate window procedure using the DispatchMessage function.

With this new alertable message loop you can now safely and efficiently use APCs from your application’s window threads to perform asynchronous I/O without needing to add additional threads to your application and thereby complicate its design and implementation.

Read part 3 now: Queuing Asynchronous Procedure Calls

© 2007 Kenny Kerr

2 Comments

  • You said, "The third parameter indicates the minimum number of milliseconds that the thread should be suspended." You meant to say "maximum".

  • Tom: it’s a bit of a niggly point but it is fact the minimum interval from the perspective of the suspension. If APCs are not queued then this is the interval before which the thread will again be schedulable and will then have to wait for a quantum so it may in fact take longer before it executes again. But I see how this could be misleading. If an APC is queued then the call will return and the interval can be seen as the maximum. As I said, a niggly point.

    The bottom line this is the time-out interval, as the documentation states. The function returns if the interval elapses even if the wake conditions have not been met.

Comments have been disabled for this content.