Cancellation of Windows Runtime activities is asynchronous

ibobev1 pts0 comments

Cancellation of Windows Runtime activities is asynchronous - The Old New Thing

Skip to main content

Dev Blogs

AI

All .NET posts

.NET MAUI<br>ASP.NET Core<br>Blazor<br>Entity Framework

C++<br>C#<br>F#<br>TypeScript

NuGet<br>Servicing<br>.NET Blog in Chinese

Microsoft for Developers<br>Agent Framework<br>Develop from the cloud<br>Xcode<br>ISE Developer<br>TypeScript<br>PowerShell<br>Python<br>Java<br>Java Blog in Chinese<br>Go<br>Microsoft Edge Dev<br>Microsoft 365 Developer<br>Microsoft Entra Identity Developer<br>Microsoft Entra PowerShell

Visual Studio<br>Visual Studio Code<br>Aspire

All things Azure<br>Azure SDK<br>Azure VM Runtime Team<br>Microsoft Azure<br>Azure Cosmos DB<br>Azure DocumentDB<br>Azure Data Studio<br>Azure SQL<br>DevOps<br>DirectX<br>Microsoft Foundry<br>Power Platform

OData<br>Unified Data Model (IDEAs)

Windows Command Line<br>#ifdef Windows<br>Inside MSIX<br>MIDI and music<br>React Native<br>The Old New Thing<br>Windows Developer

Raymond Chen

In the Windows Runtime, there are four interface patterns for representing asynchronous activity.

No return type<br>With return type T

Without progress<br>IAsyncAction<br>IAsyncOperation

With progress<br>IAsyncActionWithProgress<br>IAsyncOperationWithProgress

For the purpose of this discussion, I will collectively call these "asynchronous activities".

One of the things you can do with asynchronous activities is cancel them, by calling the Cancel method. This method submits a request to cancel, but it does not wait for the operation to acknowledge the cancellation. If you want to wait for the operation to stop executing, you have to wait for it to call the completion callback.²

Asynchronous cancellation is important for avoiding deadlocks.

Most of the time, the scenarios involve cross-thread synchronous calls, but here’s an extremely obvious way it can happen.

Suppose that you have registered a progress callback on your asynchronous activity with progress.

// C#<br>async Task DoSomethingWithTimeoutAsync()<br>var op = DoSomethingAsync();<br>op.Progress = (sender, p) => {<br>UpdateProgress(p);<br>if (p >= 0.5) {<br>sender.Cancel();<br>};<br>try {<br>await op;<br>} catch (TaskCanceledException) {<br>// ignore cancellation

// C++/WinRT<br>winrt::fire_and_forget Widget::DoSomethingWithTimeoutAsync()<br>auto op = DoSomethingAsync();<br>op.Progress([&](auto&& sender, auto p) {<br>this->UpdateProgress(p);<br>if (p >= 0.5) {<br>sender.Cancel();<br>});

try {<br>co_await op;<br>} catch (winrt::hresult_canceled const&) {<br>// ignore cancellation<br>co_return;

The code calls DoSomethingAsync() and attaches a progress callback which cancels the operation once the progress reaches 50%. If the Cancel() method waited for outstanding progress callbacks to completed, you have a deadlock: The Cancel() is waiting for the progress callback to complete. But the progress callback is itself calling Cancel().¹

To avoid deadlocks when cancellation occurs while a progress callback is in progress, the cancellation method doesn’t wait for an acknowledgment. If you want to know when the activity is finished, wait for it to complete. If you want to ignore progress reports that arrive after you cancel, you can do that yourself.

// C#

async Task DoSomethingWithTimeoutAsync()<br>var op = DoSomethingAsync();<br>bool canceled = false;<br>op.Progress = (sender, p) => {<br>if (!canceled) {<br>UpdateProgress(p);<br>if (p >= 0.5) {<br>canceled = true;<br>sender.Cancel();<br>};<br>try {<br>await op;<br>} catch (TaskCanceledException) {<br>// ignore cancellation

// C++/WinRT

winrt::fire_and_forget Widget::DoSomethingWithTimeoutAsync()<br>auto op = DoSomethingAsync();<br>bool canceled = false;<br>op.Progress([&](auto&& sender, auto p) {<br>if (!canceled) {<br>this->UpdateProgress(p);<br>if (p >= 0.5) {<br>canceled = true;<br>sender.Cancel();<br>});

try {<br>co_await op;<br>} catch (winrt::hresult_canceled const&) {<br>// ignore cancellation<br>co_return;

(The canceled variable doesn’t need to be atomic because progress callbacks do not overlap.)

Notice in the C++/winRT version that even after we call Cancel(), we wait for the co_await op to report completion before we return. Otherwise, the Progress callback will access an already-destroyed canceled variable.

¹ This is also the cancellation model for I/O and RPC: The cancellation method submits a cancellation request and returns immediately, and the underlying operation indicates that it has stopped executing by reporting some sort of completion.

² You might try to solve this by saying "Cancellation is asynchronous if the Cancel is issued from the same thread as the progress event", but that doesn’t help in this case, which is more realistic:

// C#<br>async void CancelAfter(IAsyncInfo op, TimeSpan delay)<br>co_await Task.Delay(delay);<br>op.Cancel();

async Task DoSomethingWithTimeoutAsync()<br>var op = DoSomethingAsync();<br>op.Progress = (sender, p) => {<br>Invoke(() => UpdateProgress(p));<br>};<br>CancelAfter(op, TimeSpan.FromSeconds(5));<br>try {<br>await op;<br>} catch (TaskCanceledException) {<br>// ignore cancellation

Suppose the Progress event is raised on a background thread at 4.9999 seconds. Before the lambda can call Invoke(), the Cancel­After­Delay timeout elapses, and the UI thread calls Cancel(). Now...

progress cancel cancellation sender asynchronous azure

Related Articles