Async cancellation bridge between coroutine and synchronous code
jonwis opened this issue · comments
Jon Wiswall commented
We have code that takes a shared_ptr<bool>
as a "cancel marker" for when an outer IAsyncOperation calls an inner long-running-but-non-async method, like this:
winrt::IAsyncOperation<winrt::hstring> GetStringOfManyThingsAsync() {
auto lifetime{get_strong()};
auto sharedCancel = std::make_shared<bool>(false);
auto canceltoken = co_await winrt::get_cancellation_token();
canceltoken.enable_propagation(); // not _strictly_ necessary since this does not await anything else
canceltoken.callback([sharedCancel] { *sharedCancel = true; });
co_await winrt::resume_background();
auto moreThings = CallOtherCodeSynchronously(..., sharedCancel);
co_return StringFromMoreThigns(moreThings);
}
auto CallOtherCodeSynchronously(auto... std::shared_ptr<bool> cancelToken) {
for (int i = 0; i < 200 && !*cancelToken; ++i) {
DoSlowThing();
}
return ...;
}
It'd be neat if instead we could say this:
winrt::IAsyncOperation<winrt::hstring> GetStringOfManyThingsAsync() {
auto lifetime{get_strong()};
auto canceltoken = co_await wil::get_shared_cancellation_token();
canceltoken.enable_propagation(); // not _strictly_ necessary since this does not await anything else
co_await winrt::resume_background();
auto moreThings = CallOtherCodeSynchronously(..., canceltoken);
co_return StringFromMoreThigns(moreThings);
}
auto CallOtherCodeSynchronously(auto... std::shared_ptr<bool> cancelToken) {
for (int i = 0; i < 200 && !*cancelToken; ++i) {
DoSlowThing();
}
return ...;
}
Or maybe instead we say this:
auto canceltoken = co_await winrt::get_cancellation_token();
auto sharedCanceller = wil::make_shared_cancel(canceltoken);
... where sharedCanceller is basically this:
struct shared_cancel_token
{
bool m_canceled{false};
void cancel() { m_canceled = true; }
bool is_canceled() const { return m_canceled };
}
template<typename Q> std::shared_ptr<shared_cancel_token> make_shared_cancel(Q& outerToken)
{
auto t = std::make_shared<shared_cancel_token>();
outerToken.callback([t] { t->cancel(); };
return t;
}
And then code passes around the shared_cancel_token instead of the shared_ptr. Then we could also do things like hang a wait()
off of that using WaitOnAddress (expanding the size of m_canceled to be a pointer) or expose an event handle for use with WFMO.
Jon Wiswall commented
Or maybe it's make_cancellation_adapter
since it's adapting between async & sync?
Jon Wiswall commented
Oh, maybe this is easy:
struct shared_cancel_token
{
bool is_cancelled() const
{
return m_cancelled->is_signaled();
}
void cancel()
{
m_cancelled->SetEvent();
}
bool wait_for(std::optional<std::chrono::milliseconds> timeout = std::nullopt)
{
if (timeout)
{
return m_cancelled->wait(timeout->count());
}
else
{
return m_cancelled->wait();
}
}
private:
std::shared_ptr<wil::slim_event_manual_reset> m_cancelled{std::make_shared<wil::slim_event_manual_reset>()};
};
shared_cancel_token make_shared_cancel_token(auto&& sourceToken)
{
shared_cancel_token t;
sourceToken.callback([t]() mutable { t.cancel(); });
return t;
}