How to get your C++/WinRT asynchronous operations to respond more quickly to cancellation, part 2

Raymond Chen

Raymond

We saw last time that you can hasten the cancellation of your C++/WinRT coroutine by polling for cancellation. But that works only if it’s the top-level coroutine that needs to respond to the cancellation. But often, your coroutine calls out to other coroutines, and if one of those other coroutines takes a long time, your main coroutine won’t get a chance to respond to the cancellation until it regains control.

Can we get the main coroutine to respond more quickly to cancellation?

Sure.

You can pass a custom delegate to the the C++/WinRT cancellation token’s callback method to be notified immediately when the coroutine is cancelled.

IAsyncAction ProcessAllWidgetsAsync()
{
    auto cancellation = co_await get_cancellation_token();

    cancellation.callback([] { /* zomg! cancelled! */ });

    auto widgets = co_await GetAllWidgetsAsync();
    for (auto&& widget : widgets) {
        if (cancellation()) co_return;
        ProcessWidget(widget);
    }
    co_await ReportStatusAsync(WidgetsProcessed);
}

When the coroutine is cancelled, the cancellation callback is called immediately. This is your chance to hasten the death of your coroutine. For example, we could do so by cancelling the Get­All­Widgets­Async call.

IAsyncAction ProcessAllWidgetsAsync()
{
    auto cancellation = co_await get_cancellation_token();

    auto operation = GetAllWidgetsAsync();
    cancellation.callback([operation] { operation.Cancel(); });
    auto widgets = co_await operation;

    for (auto&& widget : widgets) {
        if (cancellation()) co_return;
        ProcessWidget(widget);
    }
    co_await ReportStatusAsync(WidgetsProcessed);
}

If the Process­All­Widgets­Async is cancelled, we propagate that cancellation to the Get­All­Widgets­Async operation, in the hopes that it will abandon its attempt to get all the widgets and give control back to Process­All­Widgets­Async. The co_await will fail with hresult_canceled, which will then propagate out of the coroutine, causing the entire coroutine to become cancelled.

This is a common enough pattern that you could write a wrapper for it:

template<typename Async, typename Token>
std::decay_t<Async> MakeCancellable(Async&& async, Token&& token)
{
    token.callback([async] { async.Cancel(); });
    return std::forward<Async>(async);
}

Now we just wrap our asynchronous operations inside a Make­Cancellable:

IAsyncAction ProcessAllWidgetsAsync()
{
    auto cancellation = co_await get_cancellation_token();

    auto widgets = co_await MakeCancellable(GetAllWidgetsAsync(), cancellation);

    for (auto&& widget : widgets) {
        if (cancellation()) co_return;
        ProcessWidget(widget);
    }
    co_await MakeCancellable(ReportStatusAsync(WidgetsProcessed), cancellation);
}

Exercise: What happens if the Process­All­Widgets­Async is cancelled after Get­All­Widgets has completed?

4 comments

Comments are closed. Login to edit/delete your existing comments

    • Ian Yates
      Ian Yates

      I don’t think this is like C# where you have some kind of ambient context that can be flowed, and even in that case there’s not some cancellation token that is flowed anyway – other async state & context is but not cancellation. It’s possible there are bits you simply want to complete atomically and not be cancelled, so an auto flow of sorts would get in the way.
      The cancellation token is being fetched from the async machinery the compiler has built for the immediate method.
      Memory management in C++, rather than having a garbage collector, probably also makes automatic flowing of such things difficult

    • Raymond Chen
      Raymond ChenMicrosoft employee

      The cancellation token is a C++/WinRT feature that is not part of the Windows Runtime. It can’t be propagated to nested IAsyncActions because GetAllWidgetsAsync() could be implemented in some other language like C# or Visual Basic or C++/CX, and they don’t know about C++/WinRT cancellation tokens. To cancel a Windows Runtime asynchronous activity, you invoke the Cancel method. So we did what we had to do: Hook up the cancellation token to the Cancel method.

  • Avatar
    紅樓鍮

    Maybe Coroutines TS could have supported something like

    co_await[winrt::no_flow_context, winrt::link_cancellation] ReadAsync();

    — “configured co_await“, with the things in square brackets passed as additional arguments to __promise.await_transform() (or the awaitable’s operator co_await(), I’m not sure which would be a better design). That way our code could look a bit nicer.