July 23rd, 2020

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

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?

Don’t give up yet: There’s part 3.

Topics
Code

Author

Raymond has been involved in the evolution of Windows for more than 30 years. In 2003, he began a Web site known as The Old New Thing which has grown in popularity far beyond his wildest imagination, a development which still gives him the heebie-jeebies. The Web site spawned a book, coincidentally also titled The Old New Thing (Addison Wesley 2007). He occasionally appears on the Windows Dev Docs Twitter account to tell stories which convey no useful information.

4 comments

Discussion is closed. Login to edit/delete existing comments.

  • 紅樓鍮

    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.

  • Gunnar Dalsnes

    I don’t get it. Why isn’t the cancellation propagated by default?

    • Raymond ChenMicrosoft employee Author

      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.

      Read more
    • 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...

      Read more