为什么不等待任务。什么时候抛出一个聚合异常?

在这个代码中:

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吗?

75255 次浏览

我不太记得具体是在哪里,但是我在某个地方读到过,使用新的 异步/等待关键字,它们会将 AggregateException解包成实际的异常。

因此,在 catch 块中,得到的是实际的异常,而不是聚合的异常。这有助于我们编写更加自然和直观的代码。

这对于更容易地将现有代码转换为使用 异步/等待也是必需的,因为在 异步/等待中,大量代码需要特定的异常,而不是聚合的异常。

——编辑——

明白了:

作者: Bill Wagner

比尔 · 瓦格纳说:

... 使用 wait 时,编译器生成的代码将打开 并抛出基础异常 等待时,可以避免处理 AggreateException 类型的额外工作 中定义的 Task.Result、 Task.Wait 和其他等待方法使用 任务类。这是使用 wait 而不是使用 基本的任务方法。

你想的是 Task.WaitAll-它抛出一个 AggregateException

当 All 只是抛出它遇到的异常列表的第一个异常时。

您可以遍历所有任务,查看是否有多个任务引发了异常:

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(); });
}

我知道这是一个问题,已经回答了,但选择的答案不解决 真的的 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 属性,该属性保存 AggreateException (即使只有一个任务抛出异常)。

希望这个还有用。我知道我今天遇到了这个问题。

我只是想扩展@Richiban 的回答,说明您也可以通过从任务中引用 catch 块中的 AggreateException 来处理它。例如:

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");
}

这对我有用

private async Task WhenAllWithExceptions(params Task[] tasks)
{
var result = await Task.WhenAll(tasks);
if (result.IsFaulted)
{
throw result.Exception;
}
}

这里有很多好的答案,但是我仍然想发表我的抱怨,因为我刚刚遇到了同样的问题,并进行了一些研究。或者跳到下面的 TLDR 版本

问题是

等待 Task.WhenAll返回的 task只会抛出存储在 task.Exception中的 AggregateException的第一个异常,即使在多个任务出现故障时也是如此。

Task.WhenAll的当前文档表示:

如果提供的任何任务以错误状态完成,则 返回的任务也将以错误状态完成,其中 异常将包含取消包装集的聚合 提供的每个任务的异常。

这是正确的,但是它没有说明前面提到的等待返回任务时的“展开”行为。

我想,医生没有提到它 因为这种行为不是特定于 Task.WhenAll

很简单,Task.ExceptionAggregateException类型的,对于 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

微软的 Stephen Toub解释了 相关的 GitHub 问题中这种行为背后的原因:

我想说的是我们已经深入讨论过了, 几年前,当这些最初被添加。我们最初做什么 你是说,从 WhenAll 返回的任务包含一个 包含所有异常(即。 将返回一个 AggreateException 包装器,该包装器 包含另一个 AggreateException,然后包含实际的 异常; 然后当它被等待时,内部 AggreateException 我们收到的强烈反馈使我们 改变设计的原因是: a)绝大多数此类案件 相当同质的异常,例如在 聚合并不那么重要,b)传播聚合 打破了对特定异常类型的捕获量的预期, C)如果有人确实想要集合,他们可以这样做 如此明确地用我写的两行。我们也有广泛的 关于等待的行为应该是什么的讨论 包含多个异常的任务,这就是我们着陆的地方。

另一个需要注意的重要问题是,这种展开行为是肤浅的。也就是说,它只会打开来自 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;
  • 正确地传播取消状态(Task.IsCanceled) ,因为类似的事情不会这样做: Task t = Task.WhenAll(...); try { await t; } catch { throw 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}");
}

你真正需要做的是:

await Task.WhenAll(DoLongThingAsyncEx1(), DoLongThingAsyncEx2())
.ContinueWith(t => throw t.Exception!.Flatten(), TaskContinuationOptions.OnlyOnFaulted);