microsoft / wil

Windows Implementation Library

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Async cancellation bridge between coroutine and synchronous code

jonwis opened this issue · comments

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.

Or maybe it's make_cancellation_adapter since it's adapting between async & sync?

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;
}