我总是建议人们阅读托管线程文档中的取消。它还不够完整;像大多数 MSDN 文档一样,它告诉您可以做什么,而不是您应该做什么。但它肯定比取消的 dotnet 文档更清楚。
该示例显示了基本用法
首先,重要的是要注意示例代码中的取消仅取消任务- 它不会取消基础操作。我强烈建议您不要这样做。
如果要取消操作,则需要更新RunFoo
以获取CancellationToken
(请参阅下文了解如何使用它):
public Task Run(IFoo foo, CancellationToken token = default(CancellationToken)) {
var tcs = new TaskCompletionSource<object>();
// Regular finish handler
EventHandler<AsyncCompletedEventArgs> callback = (sender, args) =>
{
if (args.Cancelled)
{
tcs.TrySetCanceled(token);
CleanupFoo(foo);
}
else
tcs.TrySetResult(null);
};
RunFoo(foo, token, callback);
return tcs.Task;
}
如果你不能取消foo
,那么你的 API 支持根本就没有取消:
public Task Run(IFoo foo) {
var tcs = new TaskCompletionSource<object>();
// Regular finish handler
EventHandler<EventArgs> callback = (sender, args) => tcs.TrySetResult(null);
RunFoo(foo, callback);
return tcs.Task;
}
然后,调用者可以对任务执行可取消的等待,这对于这种情况来说是一种更合适的代码技术(因为取消的是等待,而不是任务所代表的操作)。可以通过我的 AsyncEx.Tasks library执行“可取消等待” ,或者您可以编写自己的等效扩展方法。
文档说要么只是返回并将任务状态切换到 RanToCompletion,要么抛出 OperationCanceledException(这导致任务的结果被取消)都可以。
是的,那些文档具有误导性。首先,请不要只是返回;您的方法将成功完成任务 - 表明操作已成功完成 - 而实际上操作并未成功完成。这可能适用于某些代码,但通常肯定不是一个好主意。
通常,响应 a 的正确方法CancellationToken
是:
- 定期调用
ThrowIfCancellationRequested
。这个选项更适合 CPU-bound 代码。
- 通过 注册取消回调
Register
。此选项更适合 I/O 绑定代码。请注意,必须处理注册!
在您的特定情况下,您遇到了不寻常的情况。在你的情况下,我会采取第三种方法:
- 在您的“每帧工作”中,检查
token.IsCancellationRequested
; 如果被请求,则将回调事件AsyncCompletedEventArgs.Cancelled
设置为true
。
这在逻辑上等价于第一种正确方式(定期调用ThrowIfCancellationRequested
),捕获异常,并将其转换为事件通知。无一例外。
如果我等待返回的任务,我总是会收到 TaskCanceledException。我的猜测是这是正常行为(我希望是这样),并且当我想使用取消时,我应该在调用周围加上 try/catch。
可以取消的任务的正确使用await
代码是将 包装在 try/catch和 catchOperationCanceledException
中。由于各种原因(许多历史原因),一些 API 会OperationCanceledException
导致TaskCanceledException
. 由于TaskCanceledException
派生自OperationCanceledException
,使用代码可以捕获更一般的异常。
但我想如果用户想要区分正常完成的任务和取消的任务,[取消异常]几乎是确保这一点的副作用,对吧?
这是公认的模式,是的。
文档表明,任何异常,即使是与取消有关的异常,都由 TPL 包装在 AggregateException 中。
仅当您的代码在任务上同步阻塞时,这才是正确的。它应该首先避免这样做。因此,文档肯定再次具有误导性。
但是,在我的测试中,我总是直接得到 TaskCanceledException,没有任何包装。
await
避免AggregateException
包装。
更新评论解释CleanupFoo
是一种取消方法。
我首先建议尝试直接在由;CancellationToken
发起的代码中使用。RunFoo
这种方法几乎肯定会更容易。
但是,如果您必须CleanupFoo
用于取消,那么您将需要Register
它。您需要处理该注册,而执行此操作的最简单方法实际上可能是将其拆分为两种不同的方法:
private Task DoRun(IFoo foo) {
var tcs = new TaskCompletionSource<object>();
// Regular finish handler
EventHandler<EventArgs> callback = (sender, args) => tcs.TrySetResult(null);
RunFoo(foo, callback);
return tcs.Task;
}
public async Task Run(IFoo foo, CancellationToken token = default(CancellationToken)) {
var tcs = new TaskCompletionSource<object>();
using (token.Register(() =>
{
tcs.TrySetCanceled(token);
CleanupFoo();
});
{
var task = DoRun(foo);
try
{
await task;
tcs.TrySetResult(null);
}
catch (Exception ex)
{
tcs.TrySetException(ex);
}
}
await tcs.Task;
}
适当地协调和传播结果——同时防止资源泄漏——是相当尴尬的。如果您的代码可以CancellationToken
直接使用,它会更干净。