一个被取消的任务传播两种不同类型的异常,这取决于它是如何等待的.为什么?



我在编写一些复杂的async/await代码时遇到了一个奇怪的行为。我无意中创造了一个被取消的具有双重(精神分裂症)身份的Task。它可以抛出TaskCanceledExceptionOperationCanceledException,这取决于我如何等待它。

  1. 等待Wait抛出AggregateException,其中包含TaskCanceledException
  2. 等待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),并且用Waitawait传播OperationCanceledException。这很令人费解,因为CancellationToken.ThrowIfCancellationRequested方法抛出OperationCanceledException,根据源代码!

下面的任务还演示了另一种不同的行为:

Task task = Task.Run(() =>
{
return Task.FromCanceled(canceledToken);
});

此任务以Canceled完成,并与Waitawait传播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),因为前者将通过继承处理两者

相关内容

  • 没有找到相关文章

最新更新