Wait for IAsyncAction on STA thread

Figured out how to elegantly do a blocking wait for an asynchronous coroutine-enabled function on a STA thread.

You can’t do this:

// /std:c++latest /await

#include <unknwn.h>
#include <winrt\base.h>
#include <winrt\Windows.Foundation.h>

#pragma comment(lib, "windowsapp.lib")

winrt::Windows::Foundation::IAsyncAction Foo()
{
    co_return;
}

int main()
{
    winrt::init_apartment(winrt::apartment_type::single_threaded);
    Foo().get(); // <<--- Debug Assertion Failed!
    return 0;
}

There is an assertion failure because .get() assumes ability to block. On STA this hits a failure in winrt::impl::blocking_suspend call.

So you have to avoid doing .get() to synchronize and there should be a message pump (you might need it for another reason anyway or why would you want non-default single_threaded in first place?).

So you would get something like this:

// /std:c++latest /await

#include <unknwn.h>
#include <winrt\base.h>
#include <winrt\Windows.Foundation.h>

#pragma comment(lib, "windowsapp.lib")

winrt::Windows::Foundation::IAsyncAction Foo()
{
    co_return;
}

int main()
{
    winrt::init_apartment(winrt::apartment_type::single_threaded);
    winrt::handle CompletionEvent { CreateEvent(nullptr, TRUE, FALSE, nullptr) };
    auto const Action { Foo() };
    Action.Completed([&](winrt::Windows::Foundation::IAsyncAction const&, winrt::Windows::Foundation::AsyncStatus Status) 
    {
        WINRT_ASSERT(Status == winrt::Windows::Foundation::AsyncStatus::Completed);
        WINRT_VERIFY(SetEvent(CompletionEvent.get()));
    });
    HANDLE const Objects[] { CompletionEvent.get() };
    for(; ; )
    {
        auto const WaitResult = MsgWaitForMultipleObjects(static_cast<DWORD>(std::size(Objects)), Objects, FALSE, INFINITE, QS_ALLEVENTS);
        if(WaitResult == WAIT_OBJECT_0 + 0) // CompletionEvent
            break;
        WINRT_ASSERT(WaitResult == WAIT_OBJECT_0 + std::size(Objects));
        MSG Message;
        while(PeekMessageW(&Message, NULL, WM_NULL, WM_NULL, PM_REMOVE))
            DispatchMessageW(&Message);
    }
    return 0;
}

Now the question is what if the Foo function needs to switch context while being on a STA thread, would it need to repeat the same pattern and dispatch messages while waiting?

NO!

Use of apartment_context enables to switch context and return back to STA in the coroutine execution sequnce, while being on outer message pump between the coroutines.

Below is full sample code that does strange threading things in the Foo function with threads and COM apartment checks, then return to calling STA in the end of the day. Additionally, it posts a message from the worker thread and makes sure that outer message pump catches it.

// /std:c++latest /await

#include <unknwn.h>
#include <winrt\base.h>
#include <winrt\Windows.Foundation.h>

#pragma comment(lib, "windowsapp.lib")

using namespace winrt::Windows::Foundation;

#include <chrono>
#include <thread>

using namespace std::chrono_literals;

void ApartmentCheck(APTTYPE ExpectType, APTTYPEQUALIFIER ExpectQualifier)
{
    APTTYPE Type;
    APTTYPEQUALIFIER Qualifier;
    WINRT_VERIFY(SUCCEEDED(CoGetApartmentType(&Type, &Qualifier)));
    WINRT_ASSERT(Type == ExpectType && Qualifier == ExpectQualifier);
}

IAsyncAction Foo()
{
    ApartmentCheck(APTTYPE_MAINSTA, APTTYPEQUALIFIER_NONE);
    winrt::apartment_context Context;
    winrt::handle ExternalEvent { CreateEvent(nullptr, TRUE, FALSE, nullptr) };
    {
        auto const ThreadIdentifier = GetCurrentThreadId();
        std::thread SimulationThread([&] 
        {
            WINRT_VERIFY(PostThreadMessageW(ThreadIdentifier, WM_APP, 0, 0));
            std::this_thread::sleep_for(5s);
            WINRT_VERIFY(SetEvent(ExternalEvent.get())); 
        });
        //co_await winrt::resume_background();
        co_await winrt::resume_on_signal(ExternalEvent.get());
        SimulationThread.join();
    }
    ApartmentCheck(APTTYPE_MTA, APTTYPEQUALIFIER_IMPLICIT_MTA); // MtaThread enables this, see below
    co_await Context;
    ApartmentCheck(APTTYPE_MAINSTA, APTTYPEQUALIFIER_NONE);
    co_return;
}

int main()
{
    winrt::init_apartment(winrt::apartment_type::single_threaded);
    winrt::handle MtaThreadTerminationEvent { CreateEvent(nullptr, TRUE, FALSE, nullptr) };
    std::thread MtaThread([&] 
    { 
        winrt::init_apartment();
        WINRT_VERIFY(WaitForSingleObject(MtaThreadTerminationEvent.get(), INFINITE) == WAIT_OBJECT_0);
    });
    std::this_thread::sleep_for(1s);
    winrt::handle CompletionEvent { CreateEvent(nullptr, TRUE, FALSE, nullptr) };
    auto const Action { Foo() };
    Action.Completed([&](IAsyncAction const&, AsyncStatus Status) 
    {
        WINRT_ASSERT(Status == AsyncStatus::Completed);
        WINRT_VERIFY(SetEvent(CompletionEvent.get()));
    });
    unsigned int MessageCount = 0;
    HANDLE const Objects[] { CompletionEvent.get() };
    for(; ; )
    {
        auto const WaitResult = MsgWaitForMultipleObjects(static_cast<DWORD>(std::size(Objects)), Objects, FALSE, INFINITE, QS_ALLEVENTS);
        if(WaitResult == WAIT_OBJECT_0 + 0) // CompletionEvent
            break;
        WINRT_ASSERT(WaitResult == WAIT_OBJECT_0 + std::size(Objects));
        MSG Message;
        while(PeekMessageW(&Message, NULL, WM_NULL, WM_NULL, PM_REMOVE))
        {
            WINRT_ASSERT(Message.message == WM_USER || Message.message == WM_APP);
            if(Message.message == WM_APP)
                MessageCount++;
            DispatchMessageW(&Message);
        }
    }
    WINRT_ASSERT(MessageCount == 1);
    WINRT_VERIFY(SetEvent(MtaThreadTerminationEvent.get()));
    MtaThread.join();
    return 0;
}

Some additional comments to the code:

  • MtaThread is necessary for thread pool threads to belong to implicit MTA or otherwise COM backed STA return would not work
  • Initial sleep is to make sure that MTA is up
  • DispatchMessageW would dispatch two messages, one that we PostThreadMessageW ourselves and the other WM_USER one which is a part of co_await Context; call)
  • SimulationThread is featuring externally set asynchronous event
  • Commented out co_await winrt::resume_background(); indicates that there is no need in explicit switch to a worker thread: coroutine tech itself would suspend execution and continue on a thread pool thread (or maybe it’s implementation specific?)

Leave a Reply