Windows Runtime 活动的取消是异步的
摘要
本文通过代码示例解释了为什么 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<T></tt></td>
</tr>
<tr>
<th>带进度</th>
<td><tt>IAsyncActionWithProgress<P></tt></td>
<td><tt>IAsyncOperationWithProgress<T, P></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>CancelAfterDelay</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 账户上,讲述一些毫无实用价值的故事。
相似文章
如果C#和JavaScript允许我多次等待Windows Runtime异步操作,为什么C++/WinRT不行?
Raymond Chen解释了为什么C++/WinRT不像C#、JavaScript和Python那样允许多次等待异步操作,其原因是没有标准库的task类型,以及不为你未使用的功能付费的原则。
理解在试图绕过规则时规则背后的原理
本文来自微软的《老东西》博客,解释了Windows内核回调函数最佳实践背后的原理,特别是为什么阻塞或等待工作项会违背其目的,并通过一个关于驱动程序导致系统挂起的警示故事来说明。
在多个协程之间共享单个Windows Runtime IAsyncOperation的结果,第3部分
本文讨论了一个C++/WinRT模式,用于缓存Windows Runtime IAsyncOperation的结果,包括处理失败的情况,以便多个协程可以共享缓存的结果或异常。
异步编程的承诺与现实
深入剖析异步编程模型的演进——从回调到 Promise——揭示每一轮迭代如何解决先前的资源与性能问题,同时带来新的易用性挑战。
如何在Windows运行时中使用Win32结构?
本文解释了如何通过声明具有相同布局的影子结构在Windows运行时中使用Win32结构,包括具体示例和常见结构的替代方案。