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 GetAllWidgetsAsync
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 ProcessAllWidgetsAsync
is cancelled, we propagate that cancellation to the GetAllWidgetsAsync
operation, in the hopes that it will abandon its attempt to get all the widgets and give control back to ProcessAllWidgetsAsync
. 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 MakeCancellable
:
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 ProcessAllWidgetsAsync
is cancelled after GetAllWidgets
has completed?
Don’t give up yet: There’s part 3.
Maybe Coroutines TS could have supported something like
— “configured
co_await
“, with the things in square brackets passed as additional arguments to__promise.await_transform()
(or the awaitable’soperator co_await()
, I’m not sure which would be a better design). That way our code could look a bit nicer.I don’t get it. Why isn’t the cancellation propagated by default?
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.
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...