在这段代码中:
private async void button1_Click(object sender, EventArgs e) {
try {
await Task.WhenAll(DoLongThingAsyncEx1(), DoLongThingAsyncEx2());
}
catch (Exception ex) {
// Expect AggregateException, but got InvalidTimeZoneException
}
}
Task DoLongThingAsyncEx1() {
return Task.Run(() => { throw new InvalidTimeZoneException(); });
}
Task DoLongThingAsyncEx2() {
return Task.Run(() => { throw new InvalidOperation();});
}
我希望 WhenAll
创建并抛出一个 AggregateException
,因为它正在等待的至少一项任务抛出了异常。相反,我正在返回由其中一项任务引发的单个异常。
WhenAll
不总是创建一个 AggregateException
吗?
AggregateException
。如果您在示例中使用 Task.Wait
而不是 await
,您将捕获 AggregateException
Task.WhenAll
的所有异常,我落入了同一个陷阱。所以我尝试了 going into deep details 关于这种行为。
Task.Wait
正在阻塞,而 await
不是。
我知道这是一个已经回答的问题,但选择的答案并不能真正解决 OP 的问题,所以我想我会发布这个。
此解决方案为您提供聚合异常(即各种任务引发的所有异常)并且不会阻塞(工作流程仍然是异步的)。
async Task Main()
{
var task = Task.WhenAll(A(), B());
try
{
var results = await task;
Console.WriteLine(results);
}
catch (Exception)
{
if (task.Exception != null)
{
throw task.Exception;
}
}
}
public async Task<int> A()
{
await Task.Delay(100);
throw new Exception("A");
}
public async Task<int> B()
{
await Task.Delay(100);
throw new Exception("B");
}
关键是在等待之前保存对聚合任务的引用,然后您可以访问它的 Exception 属性,该属性包含您的 AggregateException(即使只有一个任务引发了异常)。
希望这仍然有用。我知道我今天遇到了这个问题。
我不完全记得在哪里,但我在某个地方读到了新的 async/await 关键字,它们将 AggregateException
解包到实际的异常中。
因此,在 catch 块中,您会得到实际的异常,而不是聚合的异常。这有助于我们编写更自然和直观的代码。
这也是为了更容易地将现有代码转换为使用 async/await 所需要的,其中许多代码需要特定的异常而不是聚合的异常。
- 编辑 -
知道了:
Bill Wagner 的异步入门
Bill Wagner 说:(在When Exceptions Happen 中)......当你使用await 时,编译器生成的代码将AggregateException 解包并抛出底层异常。通过利用 await,您可以避免处理 Task.Result、Task.Wait 和 Task 类中定义的其他 Wait 方法使用的 AggregateException 类型的额外工作。这是使用 await 而不是底层 Task 方法的另一个原因....
你可以遍历所有任务,看看是否有多个任务抛出了异常:
private async Task Example()
{
var tasks = new [] { DoLongThingAsyncEx1(), DoLongThingAsyncEx2() };
try
{
await Task.WhenAll(tasks);
}
catch (Exception ex)
{
var exceptions = tasks.Where(t => t.Exception != null)
.Select(t => t.Exception);
}
}
private Task DoLongThingAsyncEx1()
{
return Task.Run(() => { throw new InvalidTimeZoneException(); });
}
private Task DoLongThingAsyncEx2()
{
return Task.Run(() => { throw new InvalidOperationException(); });
}
exceptions
包含两个抛出的异常。
await
导致第一个异常被解包,但所有异常确实仍然可以通过 Tasks 数组获得。
AggregateException
的整个聚合使用。 allTasksCompleted.Exception.InnerException
持有“第一个”异常,在 await
失败后捕获的同一个异常。但随后,遍历 allTasksCompleted.Exception.InnerException*s*
以遍历多个异常,或使用 .Flatten()
递归地将任何聚合转换为一个可枚举。
这里有很多好的答案,但我仍然想发表我的咆哮,因为我刚刚遇到了同样的问题并进行了一些研究。或跳至下方的the TLDR version。
问题
等待 Task.WhenAll
返回的 task
只会引发存储在 task.Exception
中的 AggregateException
的第一个异常,即使多个任务出现故障也是如此。
current docs for Task.WhenAll
说:
如果任何提供的任务在故障状态下完成,则返回的任务也将在故障状态下完成,其异常将包含来自每个提供的任务的未包装异常集的聚合。
这是正确的,但它没有说明上述等待返回任务时的“解包”行为。
我想,文档没有提及它,因为该行为并非特定于 Task.WhenAll
。
只是 Task.Exception
是 AggregateException
类型,并且对于 await
延续,它总是被解包为它的第一个内部异常,这是设计的。这对大多数情况都很好,因为通常 Task.Exception
只包含一个内部异常。但是考虑一下这段代码:
Task WhenAllWrong()
{
var tcs = new TaskCompletionSource<DBNull>();
tcs.TrySetException(new Exception[]
{
new InvalidOperationException(),
new DivideByZeroException()
});
return tcs.Task;
}
var task = WhenAllWrong();
try
{
await task;
}
catch (Exception exception)
{
// task.Exception is an AggregateException with 2 inner exception
Assert.IsTrue(task.Exception.InnerExceptions.Count == 2);
Assert.IsInstanceOfType(task.Exception.InnerExceptions[0], typeof(InvalidOperationException));
Assert.IsInstanceOfType(task.Exception.InnerExceptions[1], typeof(DivideByZeroException));
// However, the exception that we caught here is
// the first exception from the above InnerExceptions list:
Assert.IsInstanceOfType(exception, typeof(InvalidOperationException));
Assert.AreSame(exception, task.Exception.InnerExceptions[0]);
}
在这里,AggregateException
的实例被解包到它的第一个内部异常 InvalidOperationException
,其方式与我们对 Task.WhenAll
的处理方式完全相同。如果我们不直接通过 task.Exception.InnerExceptions
,我们可能无法观察 DivideByZeroException
。
Microsoft 的 Stephen Toub 在 the related GitHub issue 中解释了这种行为背后的原因:
我试图说明的一点是,几年前,当这些最初被添加时,它已经被深入讨论过。我们最初按照您的建议做了,从 WhenAll 返回的 Task 包含一个包含所有异常的 AggregateException ,即 task.Exception 将返回一个 AggregateException 包装器,其中包含另一个 AggregateException 然后包含实际异常;然后当它被等待时,内部的 AggregateException 将被传播。我们收到的强烈反馈导致我们改变了设计:a) 绝大多数此类案例都有相当同质的异常,因此在聚合中传播所有内容并不那么重要,b) 传播聚合然后打破对捕获的预期对于特定的异常类型,以及 c) 对于有人确实想要聚合的情况,他们可以像我写的那样使用两行明确地这样做。我们还就包含多个异常的任务的 await sould 行为进行了广泛的讨论,这就是我们着陆的地方。
另一件需要注意的重要事情是,这种展开行为很浅。即,它只会从 AggregateException.InnerExceptions
解包第一个异常并将其保留在那里,即使它恰好是另一个 AggregateException
的实例。这可能会增加另一层混乱。例如,让我们像这样更改 WhenAllWrong
:
async Task WhenAllWrong()
{
await Task.FromException(new AggregateException(
new InvalidOperationException(),
new DivideByZeroException()));
}
var task = WhenAllWrong();
try
{
await task;
}
catch (Exception exception)
{
// now, task.Exception is an AggregateException with 1 inner exception,
// which is itself an instance of AggregateException
Assert.IsTrue(task.Exception.InnerExceptions.Count == 1);
Assert.IsInstanceOfType(task.Exception.InnerExceptions[0], typeof(AggregateException));
// And now the exception that we caught here is that inner AggregateException,
// which is also the same object we have thrown from WhenAllWrong:
var aggregate = exception as AggregateException;
Assert.IsNotNull(aggregate);
Assert.AreSame(exception, task.Exception.InnerExceptions[0]);
Assert.IsInstanceOfType(aggregate.InnerExceptions[0], typeof(InvalidOperationException));
Assert.IsInstanceOfType(aggregate.InnerExceptions[1], typeof(DivideByZeroException));
}
解决方案 (TLDR)
所以,回到 await Task.WhenAll(...)
,我个人想要的是能够:
如果只抛出一个异常,则获取一个异常;
如果一个或多个任务共同引发了多个异常,则获取 AggregateException;
避免只为了检查 Task.Exception 而保存任务;
正确传播取消状态(Task.IsCanceled),因为这样的事情不会这样做:Task t = Task.WhenAll(...);尝试 { 等待 t; } 捕捉 { 抛出 t.Exception; }。
为此,我整理了以下扩展名:
public static class TaskExt
{
/// <summary>
/// A workaround for getting all of AggregateException.InnerExceptions with try/await/catch
/// </summary>
public static Task WithAggregatedExceptions(this Task @this)
{
// using AggregateException.Flatten as a bonus
return @this.ContinueWith(
continuationFunction: anteTask =>
anteTask.IsFaulted &&
anteTask.Exception is AggregateException ex &&
(ex.InnerExceptions.Count > 1 || ex.InnerException is AggregateException) ?
Task.FromException(ex.Flatten()) : anteTask,
cancellationToken: CancellationToken.None,
TaskContinuationOptions.ExecuteSynchronously,
scheduler: TaskScheduler.Default).Unwrap();
}
}
现在,以下按我想要的方式工作:
try
{
await Task.WhenAll(
Task.FromException(new InvalidOperationException()),
Task.FromException(new DivideByZeroException()))
.WithAggregatedExceptions();
}
catch (OperationCanceledException)
{
Trace.WriteLine("Canceled");
}
catch (AggregateException exception)
{
Trace.WriteLine("2 or more exceptions");
// Now the exception that we caught here is an AggregateException,
// with two inner exceptions:
var aggregate = exception as AggregateException;
Assert.IsNotNull(aggregate);
Assert.IsInstanceOfType(aggregate.InnerExceptions[0], typeof(InvalidOperationException));
Assert.IsInstanceOfType(aggregate.InnerExceptions[1], typeof(DivideByZeroException));
}
catch (Exception exception)
{
Trace.WriteLine($"Just a single exception: ${exception.Message}");
}
WithAggregatedExceptions
方法实现的一个小注释:AFAIK 条件 anteTask.Exception is AggregateException ex
将始终成功,因此它仅用于将 anteTask.Exception
分配给 ex
变量。
ex
:)
try { await source.ConfigureAwait(false); } catch { source.Wait(); }
时不会正确传播 Task.IsCanceled
。我认为它会变成Task.IsFaulted
。也许不是什么大问题,但值得关注。
async Task TestAsync() { await Task.FromException(new TaskCanceledException()); }
。 Task.IsCanceled
将在此处为 true
,就像我们刚刚在 async method
中执行 throw new TaskCanceledException()
一样。
只是想我会扩展@Richiban 的答案,说您还可以通过从任务中引用它来处理 catch 块中的 AggregateException 。例如:
async Task Main()
{
var task = Task.WhenAll(A(), B());
try
{
var results = await task;
Console.WriteLine(results);
}
catch (Exception ex)
{
// This doesn't fire until both tasks
// are complete. I.e. so after 10 seconds
// as per the second delay
// The ex in this instance is the first
// exception thrown, i.e. "A".
var firstExceptionThrown = ex;
// This aggregate contains both "A" and "B".
var aggregateException = task.Exception;
}
}
public async Task<int> A()
{
await Task.Delay(100);
throw new Exception("A");
}
public async Task<int> B()
{
// Extra delay to make it clear that the await
// waits for all tasks to complete, including
// waiting for this exception.
await Task.Delay(10000);
throw new Exception("B");
}
您正在考虑 Task.WaitAll
- 它会抛出 AggregateException
。
WhenAll 只是抛出它遇到的异常列表中的第一个异常。
WhenAll
方法返回的任务有一个 Exception
属性,它是一个 AggregateException
,其中包含在其 InnerExceptions
中引发的所有异常。这里发生的是 await
抛出第一个内部异常而不是 AggregateException
本身(如 decyclone 所说)。调用任务的 Wait
方法而不是等待它会导致引发原始异常。
WhenAll
上的 await
将解开聚合异常并将列表中的第一个异常传递给 catch。为了原始问题按预期在 catch 块中获取聚合异常,应使用 Task.WaitAll
你真正需要做的是:
await Task.WhenAll(DoLongThingAsyncEx1(), DoLongThingAsyncEx2())
.ContinueWith(t => throw t.Exception!.Flatten(), TaskContinuationOptions.OnlyOnFaulted);
这对我有用
private async Task WhenAllWithExceptions(params Task[] tasks)
{
var result = await Task.WhenAll(tasks);
if (result.IsFaulted)
{
throw result.Exception;
}
}
WhenAll
与 WhenAny
不同。 await Task.WhenAny(tasks)
将在任何任务完成后立即完成。因此,如果您有一项任务立即完成并成功,而另一项任务在引发异常之前需要几秒钟,这将立即返回而不会出现任何错误。
在您的代码中,第一个异常是按设计返回的,如 http://blogs.msdn.com/b/pfxteam/archive/2011/09/28/task-exception-handling-in-net-4-5.aspx 中所述
至于您的问题,如果您编写如下代码,您将收到 AggreateException:
try {
var result = Task.WhenAll(DoLongThingAsyncEx1(), DoLongThingAsyncEx2()).Result;
}
catch (Exception ex) {
// Expect AggregateException here
}
不定期副业成功案例分享
throw task.Exception;
放在catch
块中吗? (当实际处理异常时,看到一个空的 catch 让我很困惑。)Task.IsCanceled
) 没有正确传播。这可以使用像 this 这样的扩展帮助程序来解决。A
和B
都返回Task<int>
这有效(Task.WhenAll()
将返回Task<int[]>
)。如果A
和B
返回不同的类型,或者其中至少有一个是void
,那么您是正确的,var results = await task
将不起作用。