我在编写一些复杂的async/await代码时遇到了一个奇怪的行为。我无意中创造了一个被取消的具有双重(精神分裂症)身份的Task
。它可以抛出TaskCanceledException
或OperationCanceledException
,这取决于我如何等待它。
- 等待
Wait
抛出AggregateException
,其中包含TaskCanceledException
。 - 等待
await
抛出OperationCanceledException
。
下面是一个最小的例子,再现了这种行为:
var canceledToken = new CancellationToken(true);
Task task = Task.Run(() =>
{
throw new OperationCanceledException(canceledToken);
});
try { task.Wait(); } // First let's Wait synchronously the task
catch (AggregateException aex)
{
var ex = aex.InnerException;
Console.WriteLine($"task.Wait() failed, {ex.GetType().Name}: {ex.Message}");
}
try { await task; } // Now let's await the same task asynchronously
catch (Exception ex)
{
Console.WriteLine($"await task failed, {ex.GetType().Name}: {ex.Message}");
}
Console.WriteLine($"task.Status: {task.Status}");
输出:
task.Wait() failed, TaskCanceledException: A task was canceled.
await task failed, OperationCanceledException: The operation was canceled.
task.Status: Canceled
在小提琴上试试
谁能解释一下为什么会发生这种情况?
注:我知道TaskCanceledException
是从OperationCanceledException
衍生出来的。但是,我还是不喜欢公开一个异步API,因为它展示了如此奇怪的行为。
变体:下面的任务有一个不同的行为:
Task task = Task.Run(() =>
{
canceledToken.ThrowIfCancellationRequested();
});
这个在Faulted
状态下完成(而不是Canceled
),并且用Wait
或await
传播OperationCanceledException
。这很令人费解,因为CancellationToken.ThrowIfCancellationRequested
方法抛出OperationCanceledException
,根据源代码!
下面的任务还演示了另一种不同的行为:
Task task = Task.Run(() =>
{
return Task.FromCanceled(canceledToken);
});
此任务以Canceled
完成,并与Wait
或await
传播TaskCanceledException
。
我不知道这是怎么回事!
总结多个注释,但是:
Task.Wait()
是早于await
的旧API- 由于历史原因,
.Wait()
将显示取消为TaskCanceledException
;为了保持向后兼容性,.Wait()
在这里干预,将所有OperationCanceledException
错误暴露为TaskCanceledException
,以便现有代码继续正常工作(特别是,使现有catch (TaskCanceledException)
处理程序继续工作) await
使用不同的API;.GetAwaiter().GetResult()
,它的行为更像你所期望的和想要的(尽管在任务已知已经完成之前,预计不会使用它),但是!- 在现实中,你几乎不应该使用
.Wait()
或.GetAwaiter().GetResult()
,而应该在几乎所有情况下使用await
——参见 - 如果你正在使用异步API,并且你选择(无论出于何种原因)使用
.Wait()
或GetAwaiter().GetResult()
,那么你就进入了危险,任何后果现在都是你作为消费者的过错;不是库作者可以或应该补偿的(除了提供双同步和异步api) 特别是 - ,请注意,虽然您可能会使用
Task[<T>]
破坏侍者API,但这种带有各种其他可等待对象的模式将是一种未定义的行为(老实说,我不确定它是否真的是"定义"的)。forTask[<T>]
) - 同样:任何由"同步而非异步"引起的死锁(通常与同步上下文相关)完全是消费者在可等待结果上调用同步等待的问题
- 如果有疑问:
await
(但同样,只有await
一次;多次使用await
是也是一个未定义的行为,除了Task[<T>]
) - 用于异常处理:首选
catch (OperationCanceledException)
而不是catch (TaskCanceledException)
,因为前者将通过继承处理两者