Meet C++/WinRT 2.0: resume_foreground Improvements

It turns out that resume_foreground was being a little too clever and could introduce deadlocks in some scenarios because it only suspended if not already on the dispatcher thread. This seemed like a good idea at the time, but being able to depend on stack unwinding and re-queuing turns out to be quite important for system stability especially in OS code. Consider this simple example:

fire_and_forget UpdateAsync(TextBlock block)
{
    co_await resume_background();
    hstring message = L"Hello developers!";

    co_await resume_foreground(block.Dispatcher());
    block.Text(message);
}

Here we’re performing some complex calculation on a background thread and then naturally switch to the appropriate UI thread before updating some UI control. The resume_foreground function had some cleverness that looked something like this:

auto resume_foreground(...) noexcept
{
    struct awaitable
    {
        bool await_ready() const
        {
            return m_dispatcher.HasThreadAccess(); // <-- Cleverness...
        }
        void await_resume() const {}
        void await_suspend(coroutine_handle<> handle) const { ... }
    };
    return awaitable{ ... };
};

This has been updated as follows:

auto resume_foreground(...) noexcept
{
    struct awaitable
    {
        bool await_ready() const
        {
            return false; // <-- Queue without waiting
        }
        void await_resume() const {}
        void await_suspend(coroutine_handle<> handle) const { ... }
    };
    return awaitable{ ... };
};

This is analogous to the difference between SendMessage and PostMessage in classic desktop app development. The latter will queue the work and then unwind the stack without waiting for it to complete. This unwinding the stack can be essential.

The resume_foreground function also initially only supported the CoreDispatcher tied to a CoreWindow that was originally introduced with Windows 8. A more flexible and efficient dispatcher has since been introduced. The DispatcherQueue is nice in that you can create them for your own purposes. Consider a simple console app:

using namespace Windows::System;

fire_and_forget RunAsync(DispatcherQueue queue);

int main()
{
    auto controller = DispatcherQueueController::CreateOnDedicatedThread();
    RunAsync(controller.DispatcherQueue());
    getchar();
}

Here I’m creating a private queue thread and then passing this queue object to the coroutine. The coroutine can then presumably use it to await – suspend and resume on this private thread. Another common use of the DispatcherQueue is to create a queue on the current UI thread for a traditional desktop or Win32 app.

DispatcherQueueController CreateDispatcherQueueController()
{
    DispatcherQueueOptions options
    {
        sizeof(DispatcherQueueOptions),
        DQTYPE_THREAD_CURRENT,
        DQTAT_COM_STA
    };

    ABI::Windows::System::IDispatcherQueueController* ptr{};
    check_hresult(CreateDispatcherQueueController(options, &ptr));
    return { ptr, take_ownership_from_abi };
}

Not only does this illustrate how Win32 functions may be called and incorporated into C++/WinRT projects, by simply calling the Win32-style CreateDispatcherQueueController function to create the controller and then transferring ownership of the resulting queue controller to the caller as a WinRT object, but this is precisely how you can support efficient and seamless queuing on your existing Petzold-style Win32 desktop app:

fire_and_forget RunAsync(DispatcherQueue queue);

int main()
{
    Window window;
    auto controller = CreateDispatcherQueueController();
    RunAsync(controller.DispatcherQueue());
    MSG message;

    while (GetMessage(&message, nullptr, 0, 0))
    {
        DispatchMessage(&message);
    }
}

This simple main function starts by creating a window. You can imagine this registers a window class and calls CreateWindow to create the top-level desktop window. The CreateDispatcherQueueController function is then called to create the queue controller before calling some coroutine with the dispatcher queue owned by this controller. A traditional message pump is then entered where resumption of the coroutine naturally occurs on this thread. Having done so, you can return to the elegant world of coroutines for your async or message based workflow within your app:

fire_and_forget RunAsync(DispatcherQueue queue)
{
    ... // Start on the calling thread

    co_await resume_foreground(queue);

    ... // Resume on the dispatcher thread
}

The resume_foreground will always “queue” and then unwind the stack. You can also optionally set the resumption priority:

fire_and_forget RunAsync(DispatcherQueue queue)
{
    ...

    co_await resume_foreground(queue, DispatcherQueuePriority::High);

    ...
}

But if you only care about default queuing order then you can even await the queue itself and save yourself a few keystrokes:

fire_and_forget RunAsync(DispatcherQueue queue)
{
    ...

    co_await queue;

    ...
}

For the control freaks out there, you can even detect queue shutdown and handle that gracefully:

fire_and_forget RunAsync(DispatcherQueue queue)
{
    ...

    if (co_await queue)
    {
        ... // Resume on dispatcher thread
    }
    else
    {
        ... // Still on calling thread
    }
}

The co_await expression will return true, indicating that resumption will occur on the dispatcher thread. In other words, queuing was successful. Conversely, it will return false to indicate that execution remains on the calling thread because the queue’s controller is shutting down and is no longer serving queue requests.

As you can see, you have a great deal of power at your fingertips when you combine C++/WinRT with coroutines and especially when you do some old-school Petzold style desktop app development.

And that’s all for today. I hope you enjoy using C++/WinRT!

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s