133

在这段代码中:

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

4

8 回答 8

86

我知道这是一个已经回答的问题,但选择的答案并不能真正解决 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(即使只有一个任务引发了异常)。

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

于 2016-08-24T13:03:50.677 回答
85

我不完全记得在哪里,但是我在某个地方读到了新的async/await关键字,它们将它们解包AggregateException到实际的异常中。

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

这也是为了更容易地将现有代码转换为使用async/await所需要的,其中许多代码需要特定的异常而不是聚合的异常。

- 编辑 -

知道了:

Bill Wagner 的异步入门

比尔瓦格纳说:(在发生异常时

...当您使用 await 时,编译器生成的代码会解开 AggregateException 并引发底层异常。通过利用 await,您可以避免处理 Task.Result、Task.Wait 和 Task 类中定义的其他 Wait 方法使用的 AggregateException 类型的额外工作。这是使用 await 而不是底层 Task 方法的另一个原因....

于 2012-08-17T14:40:40.847 回答
44

你可以遍历所有任务,看看是否有多个任务抛出了异常:

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(); });
}
于 2016-01-29T07:18:07.630 回答
30

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

问题

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

当前的文档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. DivideByZeroException如果我们不直接通过,我们可能无法观察到task.Exception.InnerExceptions

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

我试图说明的一点是,几年前,当这些最初被添加时,它已经被深入讨论过。我们最初按照您的建议做了,从 WhenAll 返回的 Task 包含一个包含所有异常的 AggregateException ,即 task.Exception 将返回一个 AggregateException 包装器,其中包含另一个 AggregateException 然后包含实际异常;然后当它被等待时,内部的 AggregateException 将被传播。我们收到的强烈反馈导致我们改变了设计:a) 绝大多数此类案例都有相当同质的异常,因此在聚合中传播所有内容并不那么重要,b) 传播聚合然后打破对捕获的预期对于特定的异常类型,c)对于有人确实想要聚合的情况,他们可以像我写的那样用两行明确地这样做。我们还就包含多个异常的任务的 await sould 行为进行了广泛的讨论,这就是我们着陆的地方。

另一件需要注意的重要事情是,这种展开行为很浅。即,它只会解开第一个异常AggregateException.InnerExceptions并将其留在那里,即使它恰好是 another 的实例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唯一用于检查其Task.Exception;
  • 正确传播取消状态(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}");
}
于 2020-06-27T08:11:11.013 回答
16

只是想我会扩展@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");
}
于 2018-04-02T00:02:33.290 回答
11

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

WhenAll 只是抛出它遇到的异常列表中的第一个异常。

于 2015-04-17T04:44:58.187 回答
-3

这对我有用

private async Task WhenAllWithExceptions(params Task[] tasks)
{
    var result = await Task.WhenAll(tasks);
    if (result.IsFaulted)
    {
                throw result.Exception;
    }
}
于 2018-12-14T17:43:42.307 回答
-4

在您的代码中,第一个异常是按设计返回的,如 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
} 
于 2014-09-04T07:31:19.463 回答