Windows Runtime 活动的取消是异步的

The Old New Thing (Raymond Chen) 新闻

摘要

本文通过代码示例解释了为什么 Windows Runtime 异步活动的取消是异步的,以及它如何避免死锁,尤其是在进度回调触发取消时。

<p>在 Windows Runtime 中,有四种接口模式用于表示异步活动。</p> <table style="border-collapse: collapse;" border="1" cellspacing="0" cellpadding="3"> <tbody> <tr> <th> </th> <th>无返回类型</th> <th>带返回类型 <tt>T</tt></th> </tr> <tr> <th>无进度</th> <td><tt>IAsyncAction</tt></td> <td><tt>IAsyncOperation&lt;T&gt;</tt></td> </tr> <tr> <th>带进度</th> <td><tt>IAsyncActionWithProgress&lt;P&gt;</tt></td> <td><tt>IAsyncOperationWithProgress&lt;T, P&gt;</tt></td> </tr> </tbody> </table> <p>为了便于讨论,这里将它们统称为“异步活动”。</p> <p>你可以对异步活动执行的操作之一是取消它们,即调用 <code>Cancel</code> 方法。该方法会提交一个取消请求,但不会等待操作确认取消。如果你希望等待操作停止执行,则需要等待它调用完成回调。²</p> <p>异步取消对于避免死锁非常重要。</p> <p>大多数情况下,这些场景涉及跨线程的同步调用,但这里有一个非常明显的方式来说明它可能发生。</p> <p>假设你在带有进度的异步活动上注册了一个进度回调。</p> <pre>// C# async Task DoSomethingWithTimeoutAsync() { var op = DoSomethingAsync(); op.Progress = (sender, p) => { UpdateProgress(p); if (p >= 0.5) { sender.Cancel(); } }; try { await op; } catch (TaskCanceledException) { // 忽略取消 } } // 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&) { // 忽略取消 } co_return; } </pre> <p>代码调用了 <code>DoSomethingAsync()</code> 并附加了一个进度回调,该回调在进度达到 50% 时取消操作。如果 <code>Cancel()</code 方法等待所有未完成的进度回调完成,则会导致死锁:<code>Cancel()</code> 等待进度回调完成,而进度回调本身又在调用 <code>Cancel()</code>。¹</p> <p>为了避免当取消发生在进度回调执行期间时出现死锁,取消方法不会等待确认。如果你想知道活动何时结束,请等待它完成。如果你希望在取消后忽略到达的进度报告,可以自行处理。</p> <pre>// C# async Task DoSomethingWithTimeoutAsync() { var op = DoSomethingAsync(); <span style="border: solid 1px currentcolor;">bool canceled = false;</span> op.Progress = (sender, p) => { <span style="border: solid 1px currentcolor;">if (!canceled) {</span> UpdateProgress(p); if (p >= 0.5) { <span style="border: solid 1px currentcolor;">canceled = true;</span> sender.Cancel(); } } }; try { await op; } catch (TaskCanceledException) { // 忽略取消 } } // C++/WinRT winrt::fire_and_forget Widget::DoSomethingWithTimeoutAsync() { auto op = DoSomethingAsync(); <span style="border: solid 1px currentcolor;">bool canceled = false;</span> op.Progress([&](auto&& sender, auto p) { <span style="border: solid 1px currentcolor;">if (!canceled) {</span> this->UpdateProgress(p); if (p >= 0.5) { <span style="border: solid 1px currentcolor;">canceled = true;</span> sender.Cancel(); } } }); try { co_await op; } catch (winrt::hresult_canceled const&) { // 忽略取消 } co_return; } </pre> <p>(<code>canceled</code> 变量不需要是原子的,因为进度回调不会重叠。)</p> <p>注意,在 C++/WinRT 版本中,即使在我们调用 <code>Cancel()</code> 之后,我们也要等待 <code>co_await op</code> 报告完成,然后才返回。否则,<code>Progress</code> 回调将访问一个已被销毁的 <code>canceled</code> 变量。</p> <p>¹ 这也是 <a title="Ready. cancel. wait for it! (part 1)" href="https://devblogs.microsoft.com/oldnewthing/20110202-00/?p=11613">I/O</a> 和 <a title="Ready. cancel. wait for it! (part 3)" href="https://devblogs.microsoft.com/oldnewthing/20110204-00/?p=11583">RPC</a> 的取消模型:取消方法提交一个取消请求并立即返回,底层操作通过报告某种完成来指示它已停止执行。</p> <p>² 你可能会尝试通过说“如果 <code>Cancel</code> 是从进度事件的同一线程发出的,则取消是异步的”来解决这个问题,但这种情况无济于事,下面这个例子更现实:</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) => { Invoke(() => UpdateProgress(p)); }; CancelAfter(op, TimeSpan.FromSeconds(5)); try { await op; } catch (TaskCanceledException) { // 忽略取消 } } </pre> <p>假设进度事件在后台线程上于 4.9999 秒时被触发。在 lambda 可以调用 <code>Invoke()</code> 之前,<code>Cancel­After­Delay</code> 超时到期,UI 线程调用 <code>Cancel()</code>。现在你遇到了死锁,因为进度事件在等待 lambda,lambda 在等待 Invoke,Invoke 在等待 UI 线程,UI 线程在等待 Cancel,而 Cancel 在等待进度事件。</p> <p>本文首发于 <a href="https://devblogs.microsoft.com/oldnewthing">《老东西》</a>,原文链接:<a href="https://devblogs.microsoft.com/oldnewthing/20260624-00/?p=112465">Cancellation of Windows Runtime activities is asynchronous</a>。</p>
查看原文
查看缓存全文

缓存时间: 2026/06/25 17:10

# Windows Runtime 活动的取消是异步的 - 《The Old New Thing》 来源:https://devblogs.microsoft.com/oldnewthing/20260624-00?p=112465 在 Windows Runtime 中,有四种接口模式用于表示异步活动。 | 无返回类型 | 有返回类型 | |-----------|-----------| | 无进度 | `IAsyncAction` | `IAsyncOperation<T>` | | 有进度 | `IAsyncActionWithProgress<TProgress>` | `IAsyncOperationWithProgress<TResult, TProgress>` | 为了便于讨论,我将这些统称为“异步活动”。 你可以对异步活动执行的操作之一是取消它们,方法是调用 `Cancel` 方法。该方法提交一个取消请求,但不会等待操作确认取消。如果你希望等待操作停止执行,就必须等待它调用完成回调。 异步取消对于避免死锁非常重要。大多数情况下,这涉及跨线程的同步调用,但这里有一个极其明显的场景。假设你为带进度的异步活动注册了一个进度回调: ```csharp // C# async Task DoSomethingWithTimeoutAsync() { var op = DoSomethingAsync(); op.Progress = (sender, p) => { UpdateProgress(p); if (p >= 0.5) { sender.Cancel(); } }; try { await op; } catch (TaskCanceledException) { // 忽略取消 } } // 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&) { // 忽略取消 } co_return; } ``` 代码调用 `DoSomethingAsync()` 并附加了一个进度回调,该回调在进度达到 50% 时取消操作。如果 `Cancel()` 方法等待所有未完成的进度回调完成,就会发生死锁:`Cancel()` 正在等待进度回调完成,但进度回调本身又在调用 `Cancel()`。 为了避免在取消发生时进度回调仍在进行中导致死锁,取消方法不会等待确认。如果你想知道活动何时完成,请等待它完成。如果你希望忽略取消后到达的进度报告,可以自行处理。 ```csharp // 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) { // 忽略取消 } } // 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&) { // 忽略取消 } co_return; } ``` (`canceled` 变量不需要是原子变量,因为进度回调不会重叠。) 注意在 C++/WinRT 版本中,即使我们调用了 `Cancel()`,我们仍然等待 `co_await op` 报告完成后再返回。否则,`Progress` 回调会访问一个已经被销毁的 `canceled` 变量。 这也是 I/O 和 RPC 的取消模型:取消方法提交一个取消请求后立即返回,底层操作通过报告某种形式的完成来指示其已停止执行。 你可能会试图通过说“如果 `Cancel` 是从与进度事件相同的线程发出的,则取消是异步的”来解决这个问题,但这在这种情况下没有帮助,更具现实性的情况是: ```csharp // C# async void CancelAfter(IAsyncInfo op, TimeSpan delay) { 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) { // 忽略取消 } } ``` 假设进度事件在 4.9999 秒时由后台线程引发。在 lambda 可以调用 `Invoke()` 之前,`CancelAfterDelay` 超时发生,UI 线程调用 `Cancel()`。现在你遇到了死锁,因为进度事件在等待 lambda,lambda 在等待 Invoke,Invoke 在等待 UI 线程,UI 线程在等待 Cancel,而 Cancel 又在等待进度事件。 ### 类别 ### 主题 ## 作者 Raymond Chen Raymond 参与 Windows 的发展已超过 30 年。2003 年,他创办了一个名为 The Old New Thing 的网站,其受欢迎程度远远超出了他最疯狂的想象,这一发展至今仍让他感到忐忑不安。该网站孕育了一本书,巧合的是,书名也是《The Old New Thing》(Addison Wesley 2007)。他偶尔会出现在 Windows Dev Docs Twitter 账户上,讲述一些毫无实用价值的故事。

相似文章

理解在试图绕过规则时规则背后的原理

The Old New Thing (Raymond Chen)

本文来自微软的《老东西》博客,解释了Windows内核回调函数最佳实践背后的原理,特别是为什么阻塞或等待工作项会违背其目的,并通过一个关于驱动程序导致系统挂起的警示故事来说明。

异步编程的承诺与现实

Hacker News Top

深入剖析异步编程模型的演进——从回调到 Promise——揭示每一轮迭代如何解决先前的资源与性能问题,同时带来新的易用性挑战。

如何在Windows运行时中使用Win32结构?

The Old New Thing (Raymond Chen)

本文解释了如何通过声明具有相同布局的影子结构在Windows运行时中使用Win32结构,包括具体示例和常见结构的替代方案。