C++/WinRT: Coroutines and the Calling Context

Previous: Hosting the Windows Runtime

Let’s now return to the topic coroutines and the issue of execution or calling context. Since coroutines can be used to introduce concurrency or deal with latency in other APIs, some confusion may arise as to the execution context of a give coroutine at any given point in time. Let’s clear a few things up.

Here’s a simple function that will print out some basic information about the calling thread:

void print_context()
{
    printf("thread:%d apartment:", GetCurrentThreadId());

    APTTYPE type;
    APTTYPEQUALIFIER qualifier;
    HRESULT const result = CoGetApartmentType(&type, &qualifier);

    if (result == S_OK)
    {
        puts(type == APTTYPE_MTA ? "MTA" : "STA");
    }
    else
    {
        puts("N/A");
    }
}

That’s by no means an exhaustive or foolproof dump of apartment information, but it is good enough for our purposes today. For the COM nerds out there, N/A is “not applicable” and not the other NA that you’re thinking of. 😊 Recall that there are two primary apartment models. A process has at most one multi-threaded apartment (MTA) and may have any number of single-threaded apartments (STA). Apartments are an unfortunate reality designed to accommodate COM’s remoting architecture but traditionally used to support COM objects that were not thread safe.

The single-threaded apartment (STA) is used by COM to ensure that objects are only ever called from the thread on which they were created. Naturally, this implies some mechanism to marshal calls from other threads back on to the apartment thread. STAs typically use a message loop or dispatcher queue for this. The multi-threaded apartment (MTA) is used by COM to indicate that no thread affinity exists but that marshaling is required if a call originates in some other apartment. The relationship between objects and threads is complex, so I’ll save that for another day. The ideal scenario is when an object proclaims that it is agile and thus free from apartment affinity.

Let’s use the print_context function to write a few interesting programs. Here’s one that calls print_context before and after using resume_background, to move work on to a background (thread pool) thread.

IAsyncAction Async()
{
    print_context();
    co_await resume_background();
    print_context();
}

Consider also the following caller:

int main()
{
    init_apartment();
    Async().get();
}

The caller is important because it determines the original or calling context for the coroutine (or any function). In this case, init_apartment is called from main without any arguments. This means that the app’s primary thread will join the MTA. It thus does not require a message loop or dispatcher of any kind. It also means that the thread can happily block execution as I have done here by using the blocking get function to wait for the coroutine to complete. Since the calling thread is an MTA thread, the coroutine begins execution on the MTA. The resume_background co_await expression is used to suspend the coroutine momentarily so that the calling thread is released back to the caller and the coroutine itself is free to resume as soon as a thread is available from the thread pool so that the coroutine may continue to execute concurrently. Once on the thread pool, otherwise known as a background thread, print_context is once again called. Here’s what you might see if you were to run this program:

thread:18924 apartment:MTA
thread:9568 apartment:MTA

The thread identifiers don’t matter. What matters is that they are unique. Notice also that the thread temporarily provided by the thread pool was also an MTA thread. How can this be since we did not call init_apartment on that thread? If you step into the print_context function you will notice that the APTTYPEQUALIFIER distinguishes between these threads and identifies the thread association as being implicit. Again, I’ll leave a deeper discussion of apartments for another day. Suffice to say that you can safely assume that a thread pool thread is an MTA thread in practice, provided the process is keeping the MTA alive by some other means.

The key is that the resume_background co_await expression will effectively switch the execution context of a coroutine to the thread pool regardless of what thread or apartment it was originally executing on. A coroutine should presume that it must not block the caller and ensure that evaluation of a coroutine is suspended prior to some compute-bound operation potentially blocks the calling thread. That does not mean that resume_background should always be used. A coroutine might exist purely to aggregate some other set of coroutines. In that case, any co_await expression within the coroutine may provide the necessary suspension to ensure that the calling thread is not blocked.

Now consider what happens if we change the caller, the app’s main function as follows:

int main()
{
    init_apartment(apartment_type::single_threaded);

    Async();

    MSG message;

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

This seems reasonable enough. The primary thread becomes an STA thread, calls the Async function (without blocking in any way), and enters a message loop to ensure that the STA can service cross-apartment calls. The problem is that the MTA has not been created, so the threads owned by the thread pool will not join the MTA implicitly and can thus not make use of COM services such as activation. Here’s what you might see if you were to run the program now:

thread:17552 apartment:STA
thread:19300 apartment:N/A

Notice that following the resume_background suspension, the coroutine no longer has an apartment context in which to execute code that relies on the COM runtime. If you really need an STA for your primary thread, this problem is easily solved by ensuring that the MTA is “always on” regardless.

int main()
{
    CO_MTA_USAGE_COOKIE mta{};
    check_hresult(CoIncrementMTAUsage(&mta));
    init_apartment(apartment_type::single_threaded);

    Async();

    MSG message;

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

The CoIncrementMTAUsage function will ensure that the MTA is created. The calling thread becomes an implicit member of the MTA until or unless an explicit choice is made to join some other apartment, as is the case here. If I were to run the program again I get the desired result:

thread:11276 apartment:STA
thread:9412 apartment:MTA

This is essentially the environment in which most modern Windows apps find themselves, but now you know a bit more about how you can control or create that environment for yourself. Join me next time as we continue to explore coroutines.