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 wePostThreadMessageW
ourselves and the otherWM_USER
one which is a part ofco_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?)