C++/WinRT: Handling Async Completion

Previous: Understanding Async

Now that you have a handle on async interfaces in general, let’s begin to drill down into how they work in a bit more detail. Assuming you’re not satisfied with the blocking wait provided by the get method, what other options are there? We’ll soon switch gears and focus entirely on coroutines, but for the moment let’s take a closer look at those async interfaces to see what they offer. Both the coroutine support, as well as the get method we looked at last time, rely on the contract and state machine implied by those interfaces. I won’t go into too much detail because you really don’t need to know all that much about it, but let’s explore the basics so that it will at least be familiar if you do ever have to dive in and use them directly for something more out of the ordinary.

All four of the async interfaces logically derive from the IAsyncInfo interface. There’s very little you can do with IAsyncInfo and it’s regrettable that it even exists since it adds a bit of overhead. The only IAsyncInfo members that you should really consider are Status, which can tell you whether the async method has completed, and Cancel, which may be used to request cancellation of a long-running operation whose result is no longer needed. I nitpick this design because I really like the async pattern in general and just wish it were perfect because it is so very close.

The Status member can be useful if you need to determine whether an async method has completed without actually waiting for it. Here’s an example:

auto async = ReadAsync();

if (async.Status() == AsyncStatus::Completed)
{
    auto result = async.GetResults();
    printf("%ls\n", result.c_str());
}

Each of the four async interfaces, not IAsyncInfo itself, provide individual versions of the GetResults method that should only be called once you’ve determined that the async method has completed. Don’t confuse this with the get method provided by C++/WinRT. While GetResults is implemented by the async method itself, get is implemented by C++/WinRT. GetResults will not block if the async method is still running and will likely throw an hresult_illegal_method_call exception if called prematurely. You can no doubt begin to imagine how the blocking get method is implemented. Conceptually, it looks something like this:

auto get() const
{
    if (Status() != AsyncStatus::Completed)
    {
        // wait for completion somehow...
    }

    return GetResults();
}

The actual implementation is a bit more complicated, but this captures the gist of it. The point here is that GetResults is called regardless of whether it’s an IAsyncOperation, which returns a value, or IAsyncAction, which does not. The reason for this is that GetResults is responsible for propagating any error that may have occurred within the implementation of the async method and will rethrow an exception as needed. This is why I could simply wrap the get call inside a try-block and catch exceptions quite simply in my previous installment.

The question that remains is how the caller can wait for completion. Let’s write a non-member get function to see what’s involved. I’ll start with this basic outline, inspired by the conceptual get method above:

template <typename T>
auto get(T const& async)
{
    if (async.Status() != AsyncStatus::Completed)
    {
        // wait for completion somehow...
    }

    return async.GetResults();
}

I want this function template to work with all four of the async interfaces, so I’ll use the return statement unilaterally. Special provision is made in the C++ language for genericity and we can be thankful for that.

Each of the four async interfaces provides a unique Completed member that may be used to register a callback – called a delegate – that will be called when the async method completes. In most cases, C++/WinRT will automatically create the delegate for you. All you must do is provide some function-like handler and a lambda is usually the simplest:

async.Completed([](auto&& async, AsyncStatus status)
{
    // It's done!
});

The type of the delegate’s first parameter will be that of the async interface that just completed, but keep in mind that completion should be regarded as a simple signal. In other words, don’t stuff a bunch of code inside the Completed handler. Essentially, you should regard it as a noexcept handler because the async method will not itself know what to do with any failure occurring inside this handler. So what can we do?

Well, I might simply notify a waiting thread using an event. Here’s what our get function might look like:

template <typename T>
auto get(T const& async)
{
    if (async.Status() != AsyncStatus::Completed)
    {
        handle signal = CreateEvent(nullptr, true, false, nullptr);

        async.Completed([&](auto&&, auto&&)
        {
            SetEvent(signal.get());
        });

        WaitForSingleObject(signal.get(), INFINITE);
    }

    return async.GetResults();
}

C++/WinRT’s get methods use a condition variable with a slim reader/writer lock because it’s slightly more efficient. Such a variant might look something like this:

template <typename T>
auto get(T const& async)
{
    if (async.Status() != AsyncStatus::Completed)
    {
        slim_mutex m;
        slim_condition_variable cv;
        bool completed = false;

        async.Completed([&](auto&&, auto&&)
        {
            {
                slim_lock_guard const guard(m);
                completed = true;
            }

            cv.notify_one();
        });

        slim_lock_guard guard(m);
        cv.wait(m, [&] { return completed; });
    }

    return async.GetResults();
}

You can of course use the C++ standard library’s mutex and condition variable if you prefer. The point here is simply that the Completed handler is your hook to wiring up async completion and it can be done quite generically.

Naturally, there’s no reason for you to write your own get function and more than likely coroutines will be much simpler and more versatile in general. Still, I hope this helps you to appreciate some of the power and flexibility in the Windows Runtime.

That’s all I have time for today. Join me next time as we explore more about async in the Windows Runtime.