Cancellation of Windows Runtime activities is asynchronous

The Old New Thing (Raymond Chen) News

Summary

This article explains why cancellation of Windows Runtime asynchronous activities is asynchronous, using code examples to illustrate how it avoids deadlocks, especially when progress callbacks trigger cancellation.

<p>In the Windows Runtime, there are four interface patterns for representing asynchronous activity.</p> <table style="border-collapse: collapse;" border="1" cellspacing="0" cellpadding="3"> <tbody> <tr> <th> </th> <th>No return type</th> <th>With return type <tt>T</tt></th> </tr> <tr> <th>Without progress</th> <td><tt>IAsyncAction</tt></td> <td><tt>IAsyncOperation&lt;T&gt;</tt></td> </tr> <tr> <th>With progress</th> <td><tt>IAsyncActionWithProgress&lt;P&gt;</tt></td> <td><tt>IAsyncOperationWithProgress&lt;T, P&gt;</tt></td> </tr> </tbody> </table> <p>For the purpose of this discussion, I will collectively call these &#8220;asynchronous activities&#8221;.</p> <p>One of the things you can do with asynchronous activities is cancel them, by calling the <code>Cancel</code> 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.²</p> <p>Asynchronous cancellation is important for avoiding deadlocks.</p> <p>Most of the time, the scenarios involve cross-thread synchronous calls, but here&#8217;s an extremely obvious way it can happen.</p> <p>Suppose that you have registered a progress callback on your asynchronous activity with progress.</p> <pre>// C# async Task DoSomethingWithTimeoutAsync() { var op = DoSomethingAsync(); op.Progress = (sender, p) =&gt; { UpdateProgress(p); if (p &gt;= 0.5) { sender.Cancel(); } }; try { await op; } catch (TaskCanceledException) { // ignore cancellation } } // C++/WinRT winrt::fire_and_forget Widget::DoSomethingWithTimeoutAsync() { auto op = DoSomethingAsync(); op.Progress([&amp;](auto&amp;&amp; sender, auto p) { this-&gt;UpdateProgress(p); if (p &gt;= 0.5) { sender.Cancel(); } }); try { co_await op; } catch (winrt::hresult_canceled const&amp;) { // ignore cancellation } co_return; } </pre> <p>The code calls <code>DoSomethingAsync()</code> and attaches a progress callback which cancels the operation once the progress reaches 50%. If the <code>Cancel()</code> method waited for outstanding progress callbacks to completed, you have a deadlock: The <code>Cancel()</code> is waiting for the progress callback to complete. But the progress callback is itself calling <code>Cancel()</code>.¹</p> <p>To avoid deadlocks when cancellation occurs while a progress callback is in progress, the cancellation method doesn&#8217;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.</p> <pre>// C# async Task DoSomethingWithTimeoutAsync() { var op = DoSomethingAsync(); <span style="border: solid 1px currentcolor;">bool canceled = false;</span> op.Progress = (sender, p) =&gt; { <span style="border: solid 1px currentcolor;">if (!canceled) {</span> UpdateProgress(p); if (p &gt;= 0.5) { <span style="border: solid 1px currentcolor;">canceled = true;</span> sender.Cancel(); } } }; try { await op; } catch (TaskCanceledException) { // ignore cancellation } } // C++/WinRT winrt::fire_and_forget Widget::DoSomethingWithTimeoutAsync() { auto op = DoSomethingAsync(); <span style="border: solid 1px currentcolor;">bool canceled = false;</span> op.Progress([&amp;](auto&amp;&amp; sender, auto p) { <span style="border: solid 1px currentcolor;">if (!canceled) {</span> this-&gt;UpdateProgress(p); if (p &gt;= 0.5) { <span style="border: solid 1px currentcolor;">canceled = true;</span> sender.Cancel(); } } }); try { co_await op; } catch (winrt::hresult_canceled const&amp;) { // ignore cancellation } co_return; } </pre> <p>(The <code>canceled</code> variable doesn&#8217;t need to be atomic because progress callbacks do not overlap.)</p> <p>Notice in the C++/winRT version that even after we call <code>Cancel()</code>, we wait for the <code>co_await op</code> to report completion before we return. Otherwise, the <code>Progress</code> callback will access an already-destroyed <code>canceled</code> variable.</p> <p>¹ This is also the cancellation model for <a title="Ready. cancel. wait for it! (part 1)" href="https://devblogs.microsoft.com/oldnewthing/20110202-00/?p=11613"> I/O</a> and <a title="Ready. cancel. wait for it! (part 3)" href="https://devblogs.microsoft.com/oldnewthing/20110204-00/?p=11583"> RPC</a>: 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.</p> <p>² You might try to solve this by saying &#8220;Cancellation is asynchronous if the <code>Cancel</code> is issued from the same thread as the progress event&#8221;, but that doesn&#8217;t help in this case, which is more realistic:</p> <pre>// C# async void CancelAfter(IAsyncInfo op, TimeSpan delay) { co_await Task.Delay(delay); op.Cancel(); } async Task DoSomethingWithTimeoutAsync() { var op = DoSomethingAsync(); op.Progress = (sender, p) =&gt; { Invoke(() =&gt; UpdateProgress(p)); }; CancelAfter(op, TimeSpan.FromSeconds(5)); try { await op; } catch (TaskCanceledException) { // ignore cancellation } } </pre> <p>Suppose the Progress event is raised on a background thread at 4.9999 seconds. Before the lambda can call <code>Invoke()</code>, the <code>Cancel­After­Delay</code> timeout elapses, and the UI thread calls <code>Cancel()</code>. Now you have a deadlock because the Progress event is waiting for the lambda, the lambda is waiting for the Invoke, the Invoke is waiting for the UI thread, the UI thread is waiting for the Cancel, and the Cancel is waiting for the Progress event.</p> <p>The post <a href="https://devblogs.microsoft.com/oldnewthing/20260624-00/?p=112465">Cancellation of Windows Runtime activities is asynchronous</a> appeared first on <a href="https://devblogs.microsoft.com/oldnewthing">The Old New Thing</a>.</p>
Original Article
View Cached Full Text

Cached at: 06/25/26, 05:10 PM

# Cancellation of Windows Runtime activities is asynchronous - The Old New Thing Source: [https://devblogs.microsoft.com/oldnewthing/20260624-00?p=112465](https://devblogs.microsoft.com/oldnewthing/20260624-00?p=112465) In the Windows Runtime, there are four interface patterns for representing asynchronous activity\. No return typeWith return typeTWithout progressIAsyncActionIAsyncOperation<T\>With progressIAsyncActionWithProgress<P\>IAsyncOperationWithProgress<T, P\>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# async Task DoSomethingWithTimeoutAsync() { var op = DoSomethingAsync(); op.Progress = (sender, p) => { UpdateProgress(p); if (p >= 0.5) { sender.Cancel(); } }; try { await op; } catch (TaskCanceledException) { // ignore cancellation } } // C++/WinRT winrt::fire_and_forget Widget::DoSomethingWithTimeoutAsync() { auto op = DoSomethingAsync(); op.Progress([&](auto&& sender, auto p) { this->UpdateProgress(p); if (p >= 0.5) { sender.Cancel(); } }); try { co_await op; } catch (winrt::hresult_canceled const&) { // ignore cancellation } 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() { var op = DoSomethingAsync(); bool canceled = false; op.Progress = (sender, p) => { if (!canceled) { UpdateProgress(p); if (p >= 0.5) { canceled = true; sender.Cancel(); } } }; try { await op; } catch (TaskCanceledException) { // ignore cancellation } } // C++/WinRT winrt::fire_and_forget Widget::DoSomethingWithTimeoutAsync() { auto op = DoSomethingAsync(); bool canceled = false; op.Progress([&](auto&& sender, auto p) { if (!canceled) { this->UpdateProgress(p); if (p >= 0.5) { canceled = true; sender.Cancel(); } } }); try { co_await op; } catch (winrt::hresult_canceled const&) { // ignore cancellation } 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](https://devblogs.microsoft.com/oldnewthing/20110202-00/?p=11613)and[RPC](https://devblogs.microsoft.com/oldnewthing/20110204-00/?p=11583): 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# async void CancelAfter(IAsyncInfo op, TimeSpan delay) { co_await Task.Delay(delay); op.Cancel(); } async Task DoSomethingWithTimeoutAsync() { var op = DoSomethingAsync(); op.Progress = (sender, p) => { Invoke(() => UpdateProgress(p)); }; CancelAfter(op, TimeSpan.FromSeconds(5)); try { await op; } catch (TaskCanceledException) { // 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 you have a deadlock because the Progress event is waiting for the lambda, the lambda is waiting for the Invoke, the Invoke is waiting for the UI thread, the UI thread is waiting for the Cancel, and the Cancel is waiting for the Progress event\. ### Category ### Topics ## Author ![Raymond Chen](https://devblogs.microsoft.com/oldnewthing/wp-content/uploads/sites/38/2019/02/RaymondChen_5in-150x150.jpg) 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\.

Similar Articles

Understanding the rationale behind a rule when trying to circumvent it

The Old New Thing (Raymond Chen)

This article from Microsoft's Old New Thing blog explains the rationale behind best practices for Windows kernel callback functions, particularly why blocking or waiting on work items defeats their purpose, using a cautionary tale about drivers causing system hangs.

The Tokio/Rayon Trap and Why Async/Await Fails Concurrency

Hacker News Top

The article examines how async/await syntax, while easy to write, creates significant complexity in production by conflating asynchrony with concurrency, often requiring manual partitioning of I/O and compute tasks across separate runtimes like Tokio and Rayon, leading to latency spikes and system instability.