为啥不等待 Task.WhenAll 抛出 AggregateException?

Posted

技术标签:

【中文标题】为啥不等待 Task.WhenAll 抛出 AggregateException?【英文标题】:Why doesn't await on Task.WhenAll throw an AggregateException?为什么不等待 Task.WhenAll 抛出 AggregateException? 【发布时间】:2012-08-14 00:28:11 【问题描述】:

在这段代码中:

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

【问题讨论】:

WhenAll 确实 创建一个AggregateException。如果您在示例中使用 Task.Wait 而不是 await,您将捕获 AggregateException +1,这就是我想要弄清楚的,为我节省数小时的调试和谷歌搜索时间。 多年来我第一次需要来自Task.WhenAll 的所有异常,但我落入了同一个陷阱。所以我尝试了going into deep details 关于这种行为。 相关:I want await to throw AggregateException, not just the first Exception @PeterRitchie 这是真的,但请注意Task.Wait 是阻塞的,await 不是。 【参考方案1】:

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

问题

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

current docs for 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

Microsoft 的 Stephen Toub 在 the related GitHub issue 中解释了这种行为背后的原因:

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

需要注意的另一件重要事情是,这种展开行为是肤浅的。即,它只会从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");

【讨论】:

很棒的答案 关于WithAggregatedExceptions 方法实现的一个小注释:AFAIK 条件anteTask.Exception is AggregateException ex 将始终成功,因此它仅用于将anteTask.Exception 分配给ex 变量。 @TheodorZoulias,确实!我只是想要一个单行来介绍ex :) 我认为Task.IsCanceled 在使用try await source.ConfigureAwait(false); catch source.Wait(); 时不会像this 一样被正确传播。我想它会变成Task.IsFaulted。也许不是什么大问题,但值得关注。 顺便说一句,AFAIK 异步方法永远不会产生具有(最终)取消状态的任务 - 不确定我是否遵循,但这个可以:async Task TestAsync() await Task.FromException(new TaskCanceledException()); Task.IsCanceled 在这里将是 true,就像我们刚刚在 async method 中执行 throw new TaskCanceledException() 一样。【参考方案2】:

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

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

【讨论】:

优秀的明确答案,这应该是 IMO 的选择。 +1,但您不能简单地将throw task.Exception; 放在catch 块内吗? (当实际处理异常时,看到一个空的 catch 让我很困惑。) 这种方法的一个小缺点是取消状态 (Task.IsCanceled) 没有得到正确传播。这可以使用像 this 这样的扩展助手来解决。 我可能误读了一些东西,但是你怎么能做 var results =await task;当Task.WhenAll()返回Task,那么等待它返回void? @DavidJacobsen 取决于你传入的任务类型;因为在这种情况下AB 都返回Task&lt;int&gt; 这有效(Task.WhenAll() 将返回Task&lt;int[]&gt;)。如果AB 返回不同的类型,或者其中至少一个是void,那么你是正确的,var results = await task 将不起作用。【参考方案3】:

这对我有用

private async Task WhenAllWithExceptions(params Task[] tasks)

    var result = await Task.WhenAll(tasks);
    if (result.IsFaulted)
    
                throw result.Exception;
    

【讨论】:

WhenAllWhenAny 不同。 await Task.WhenAny(tasks) 将在任何任务完成后立即完成。因此,如果您有一项任务立即完成并成功,而另一项任务在引发异常之前需要几秒钟,这将立即返回而不会出现任何错误。 那么 throw line 将永远不会被击中 - WhenAll 会抛出异常【参考方案4】:

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

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

【讨论】:

前两个cmets不正确。代码确实有效,exceptions 包含两个抛出的异常。 DoLongThingAsyncEx2() 必须抛出 new InvalidOperationException() 而不是 new InvalidOperation() 为了减轻这里的任何疑问,我整理了一个扩展的小提琴,希望能准确地展示这种处理方式:dotnetfiddle.net/X2AOvM。您可以看到await 导致第一个异常被解包,但所有异常确实仍然可以通过Tasks 数组获得。 @nuclearpidgeon 虽然您的小提琴和 OP 的解决方案都有效,但它们要求您跟踪任务,并忽略 AggregateException 的整个聚合使用。 allTasksCompleted.Exception.InnerException 持有“第一个”异常,在 await 失败后捕获的同一个异常。但随后,遍历allTasksCompleted.Exception.InnerException*s* 以遍历多个异常,或使用.Flatten() 递归地将任何聚合转换为一个可枚举。【参考方案5】:

只是想我会扩展@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");

【讨论】:

【参考方案6】:

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

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

这也是将现有代码更轻松地转换为使用 async/await 所必需的,其中大量代码需要特定异常而不是聚合异常。

-- 编辑--

知道了:

An Async Primer by Bill Wagner

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

...当你使用 await 时,编译器生成的代码会解开 AggregateException 并引发底层异常。通过利用 等待,您避免了处理 AggregateException 类型的额外工作 由 Task.Result、Task.Wait 和在 任务类。这是使用 await 而不是 底层任务方法....

【讨论】:

是的,我知道异常处理发生了一些变化,但 Task.WhenAll 的最新文档状态“如果任何提供的任务在错误状态下完成,返回的任务也将在故障状态,其异常将包含来自每个提供的任务的一组未包装异常的聚合”....在我的情况下,我的两个任务都在故障状态下完成... @MichaelRayLovett:您没有将返回的任务存储在任何地方。我敢打赌,当您查看该任务的 Exception 属性时,您会得到一个 AggregateException。但是,在您的代码中,您使用的是等待。这使得 AggregateException 被解包到实际的异常中。 我也想过,但出现了两个问题:1)我似乎无法弄清楚如何存储任务以便我可以检查它(即“Task myTask = await Task. WhenAll(...)" 似乎不起作用。和 2) 我想我不明白 await 如何将多个异常表示为一个异常.. 它应该报告哪个异常?随便选一个? 是的,当我存储任务并在等待的 try/catch 中检查它时,我看到它的异常是 AggregatedException。所以我读的文档是对的; Task.WhenAll 将异常包装在 AggregateException 中。但随后等待正在解开它们。我现在正在阅读您的文章,但我还没有看到 await 如何从 AggregateExceptions 中选择一个异常并抛出该异常与另一个异常.. 阅读文章,谢谢。但我仍然不明白为什么 await 将 AggregateException (表示多个异常)表示为一个异常。那如何全面处理异常? .. 我想如果我想确切地知道哪些任务引发了异常以及它们引发了哪些异常,我将不得不检查由 Task.WhenAll 创建的 Task 对象??【参考方案7】:

您正在考虑Task.WaitAll - 它会抛出AggregateException

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

【讨论】:

这是错误的,从WhenAll 方法返回的任务有一个Exception 属性,它是一个AggregateException,包含在其InnerExceptions 中抛出的所有异常。这里发生的是 await 抛出第一个内部异常而不是 AggregateException 本身(就像 decyclone 所说)。调用任务的Wait 方法而不是等待它会导致抛出原始异常。 其实这个答案和之前的评论都是准确的。 WhenAll 上的 await 将解开聚合异常并将列表中的第一个异常传递给 catch。为了原始问题按预期在 catch 块中获取聚合异常,应使用 Task.WaitAll【参考方案8】:

在您的代码中,第一个异常是按设计返回的,如 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
 

【讨论】:

以上是关于为啥不等待 Task.WhenAll 抛出 AggregateException?的主要内容,如果未能解决你的问题,请参考以下文章

Task.WhenAll 不等待任务完成[关闭]

忽略在 Task.WhenAll 抛出异常的任务,只获取完成的结果

异步/等待死锁 Task.WaitAll 与 Task.WhenAll [重复]

可以使用 Task.WhenAll(...) 或其他比每次等待更有意义的东西重写此异步/等待代码吗? [复制]

使用 Task.WhenAll 一次向我的 WebAPI 发送多个请求

在一定时间后取消 Task.WhenAll