C++/WinRT: Understanding Async

Previous: Working with Strings

The Windows Runtime has a relatively simple async model in the sense that, like everything else in the Windows Runtime, it is focused on allowing components to expose async methods and making it simple for apps to call those async methods. It does not in itself provide a concurrency runtime or even anything in the way of building blocks for producing or consuming async methods. Instead, all of that is left up to the individual language projections. This is as it should be and is not meant to trivialize the Windows Runtime’s async pattern. It is no small feat to implement this pattern correctly. Of course, it also means that a developer’s perception of async in the Windows Runtime is very heavily influenced by their language of choice. A developer that has only ever used C++/CX might for example wrongly, but understandably, assume that async is a hot mess.

The ideal concurrency framework for the C# developer will be different to the ideal concurrency library for the C++ developer. The role of the language projection then is to take care of the mechanics of the async pattern and provide a natural bridge to a language-specific implementation.

Coroutines are the preferred abstraction for both implementing and calling async methods in C++, but first let’s make sure we understand how the async model works. Consider a class with a single static method that looks something like this:

struct Sample
{
    Sample() = delete;

    static Windows::Foundation::IAsyncAction CopyAsync();
};

Async methods end with “Async” by convention, so you might think of this as the Copy async method. There might be a blocking or synchronous alternative that is simply called Copy. It is conceivable that a caller might want a blocking Copy method for use by a background thread and a non-blocking, or asynchronous, method for use by a UI thread that cannot afford to block for fear of appearing unresponsive.

At first, the CopyAsync method may seem quite simple to call. I might write the following C++ code:

IAsyncAction async = Sample::CopyAsync();

As you might imagine, the resulting IAsyncAction is not actually the ultimate result of the async method, even as it is the result of calling the CopyAsync method in a traditional procedural manner. The IAsyncAction is the object that a caller may use to wait upon the result synchronously or asynchronously, depending on the situation. Along with IAsyncAction, there are three other well-known interfaces that follow a similar pattern and offer different features for the callee to communicate information back to the caller. The following table provides a comparison of the four async interfaces.

In C++ terms, the interfaces can be expressed as follows:

namespace Windows::Foundation
{
    struct IAsyncAction;

    template <typename Progress>
    struct IAsyncActionWithProgress;

    template <typename Result>
    struct IAsyncOperation;

    template <typename Result, typename Progress>
    struct IAsyncOperationWithProgress;
}

IAsyncAction and IAsyncActionWithProgress can be waited upon to determine when the async method completes, but these interfaces do not offer any observable result or return value directly. IAsyncOperation and IAsyncOperationWithProgress, on the other hand, expect the Result type parameter to indicate the type of result that can be expected when the async method completes successfully. Finally, IAsyncActionWithProgress and IAsyncOperationWithProgress expect the Progress type parameter to indicate the type of progress information that can be expected periodically for long-running operations up until the async method completes.

There are a few ways to wait upon the result of an async method. I won’t describe them all here since that would turn this into a very long article. Instead, I’ll save those for next time so that I can give them each the attention they deserve. While there are a variety of ways to handle async completion, there are only two that I recommend. Those two are the async.get() method, which performs a blocking wait, and the co_await async expression, which performs a cooperative wait in the context of a coroutine. Neither is better than the other as they simply serve different purposes. Let’s look at blocking wait today.

As I mentioned, a blocking wait can be achieved using the get() method as follows:

IAsyncAction async = Sample::CopyAsync();

async.get();

There’s seldom any value in holding on to the async object and the following form is thus preferred:

Sample::CopyAsync().get();

It’s important to keep in mind that the get method will block the calling thread until the async method completes. As such, it is not appropriate to use the get method on a UI thread since it may cause the app to become unresponsive. An assertion will fire in unoptimized builds if you attempt to do so. The get method is ideal for console apps or background threads where you may not want to use a coroutine for whatever reason.

Once the async method completes, the get method will return any result directly to the caller. In the case of IAsyncAction and IAsyncActionWithProgress, the return type is void. That might be useful for an async method that initiates a file copy operation, but less so for something like an async method that reads the contents of a file. Let’s add another async method to our example:

struct Sample
{
    Sample() = delete;

    static Windows::Foundation::IAsyncAction CopyAsync();
    static Windows::Foundation::IAsyncOperation<hstring> ReadAsync();
};

In the case of ReadAsync, the get method will properly forward the hstring result to the caller once the operation completes:

Sample::CopyAsync().get();

hstring result = Sample::ReadAsync().get();

Assuming execution returns from the get method, the resulting string will hold whatever value was returned by the async method upon its successful completion. Execution may not return, for example, if an error occurred but we’ll talk more about error handling later.

The get method is limited in the sense that it cannot be used from a UI thread, nor does it exploit the full potential of the machine’s concurrency, since it holds the calling thread hostage until the async method completes. Using a coroutine allows the async method to complete without holding such a precious resource captive for some indeterminate amount of time.

Join me next time as we explore more about async in the Windows Runtime.