为什么不等待Task.WhenAll抛出AggregateException?

Mic*_*ett 83 .net asynchronous exception tap

在这段代码中:

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();});
}
Run Code Online (Sandbox Code Playgroud)

我期望WhenAll创建并抛出一个AggregateException,因为至少有一个等待抛出异常的任务.相反,我正在收回其中一个任务抛出的单个异常.

难道WhenAll不总是创造AggregateException

dec*_*one 64

我并不完全记得在哪里,但我在某处读到了新的async/await关键字,他们将其解包AggregateException到实际的异常中.

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

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

- 编辑 -

得到它了:

Bill Wagner的Async Primer

比尔瓦格纳说:(在例外情况下发生)

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

  • @MichaelRayLovett:你没有将返回的Task存储在任何地方.我打赌当你查看该任务的Exception属性时,你会得到一个AggregateException.但是,在您的代码中,您正在使用等待.这使得AggregateException被解包到实际的异常中. (4认同)
  • 是的,我知道异常处理有一些变化,但Task.WhenAll的最新文档声明"如果任何提供的任务在故障状态下完成,返回的任务也将在故障状态下完成,其异常将包含从每个提供的任务中汇总一组未包装的异常"....在我的情况下,我的两个任务都在故障状态下完成...... (3认同)
  • 我也想到了这一点,但是出现了两个问题:1)我似乎无法弄清楚如何存储任务以便我可以检查它(即"任务myTask = await Task.WhenAll(...)"没有似乎工作.2)我想我不知道await如何代表多个异常只是一个例外..它应该报告哪个例外?随机挑一个? (3认同)
  • 阅读文章,谢谢。但是我仍然不明白为什么 await 将 AggregateException(表示多个异常)表示为一个异常。那如何全面处理异常呢?.. 我想如果我想确切地知道哪些任务抛出了异常以及哪些任务抛出了异常,我将不得不检查 Task.WhenAll 创建的 Task 对象? (3认同)
  • 是的,当我存储任务并在await的try/catch中检查它时,我发现它的异常是AggregatedException.所以我读到的文档是正确的; Task.WhenAll正在汇总AggregateException中的异常.但随后等待解开它们.我现在正在阅读你的文章,但是我还没有看到await如何从AggregateExceptions中选择一个异常并将其抛出另一个异常. (2认同)
  • 是的,您需要检查返回的任务。尽可能简单地说,在不使用 async/await 的代码中,您一次处理多少个异常?答案可能是一。它通常是最后抛出且未处理的异常。通过等待,他们似乎正在创建完全相同的场景。如果您想要高级控制,您始终可以检查返回任务的 Exception 属性。 (2认同)
  • 谢谢,我现在明白了。这篇文章非常清楚地说明了事情:http://blogs.msdn.com/b/pfxteam/archive/2011/09/28/10217876.aspx (2认同)

Ric*_*ban 38

我知道这是一个已经回答的问题,但所选择的答案并没有真正解决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");
}
Run Code Online (Sandbox Code Playgroud)

关键是在等待它之前保存对聚合任务的引用,然后您可以访问其Exception属性,该属性保存您的AggregateException(即使只有一个任务引发了异常).

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

  • +1,但是你不能简单地将“throw task.Exception;”放在“catch”块中吗?(当实际处理异常时,看到空的 catch 使我感到困惑。) (4认同)
  • @DavidJacobsen这取决于您传入的任务类型;因为在这种情况下,“A”和“B”都返回“Task&lt;int&gt;”,所以这是有效的(“Task.WhenAll()”将返回“Task&lt;int[]&gt;”)。如果“A”和“B”返回不同的类型,或者至少其中之一是“void”,那么您是正确的,“var results = wait task”将不起作用。 (3认同)
  • 这种方法的一个小缺点是取消状态(“Task.IsCanceled”)无法正确传播。这可以使用像[this](/sf/answers/4382525031/)这样的扩展助手来解决。 (2认同)
  • 我可能误读了一些东西,但是你怎么能做 var results =await task; 当 Task.WhenAll() 返回 Task 时,等待它返回 void? (2认同)

jga*_*fin 28

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

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(); });
}
Run Code Online (Sandbox Code Playgroud)

  • 前两条评论不正确.实际上代码确实工作,而`exceptions`包含抛出的两个异常. (14认同)
  • 为了缓解这里的疑问,我整理了一个扩展的小提琴,希望确切显示这种处理的效果:https://dotnetfiddle.net/X2AOvM。您可以看到`await'导致第一个异常被解包,但是所有异常确实仍然可以通过Tasks数组获得。 (6认同)
  • 这不起作用.`WhenAll`在第一个异常时退出并返回.请参阅:/sf/ask/428638451/ (2认同)

nos*_*tio 24

这里有很多很好的答案,但我仍然想发表我的咆哮,因为我刚刚遇到了同样的问题并进行了一些研究。或者跳到下面的 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]);
}
Run Code Online (Sandbox Code Playgroud)

在这里,一个 的实例AggregateExceptionInvalidOperationException与我们可能在Task.WhenAll. DivideByZeroException如果我们不task.Exception.InnerExceptions直接通过,我们可能无法观察。

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

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

需要注意的另一件重要事情是,这种展开行为是浅薄的。即,它只会解开第一个异常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));
}
Run Code Online (Sandbox Code Playgroud)

解决方案 (TLDR)

所以,回到await Task.WhenAll(...),我个人想要的是能够:

  • 如果只抛出一个异常,则获取一个异常;
  • AggregateException如果一个或多个任务共同抛出了多个异常,则获取一个;
  • 避免必须保存Taskonly 以检查其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();
    }    
}
Run Code Online (Sandbox Code Playgroud)

现在,以下工作按我想要的方式工作:

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}");
}
Run Code Online (Sandbox Code Playgroud)

  • 很棒的答案 (2认同)
  • 顺便说一句,*AFAIK 异步方法永远不会产生具有(最终)取消状态的任务* - 不确定我是否遵循,但这个方法确实如此: `async Task TestAsync() { wait Task.FromException(new TaskCanceledException()); }`。这里的“Task.IsCanceled”将为“true”,就像我们刚刚在“异步方法”中“抛出 new TaskCanceledException()”一样。 (2认同)

小智 11

你在考虑Task.WaitAll- 它会抛出一个AggregateException.

WhenAll只抛出它遇到的异常列表的第一个异常.

  • `Task.WaitAll()`也会阻塞当前线程. (13认同)
  • 这是错误的,从WhenAll方法返回的任务具有一个Exception属性,该属性是一个AggregateException,包含其InnerExceptions中引发的所有异常。这里发生的是`await`抛出了第一个内部异常而不是`AggregateException`本身(如decyclone所说)。调用任务的“ Wait”方法而不是等待它会引发原始异常。 (2认同)

小智 9

我想我会扩展@ 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");
}
Run Code Online (Sandbox Code Playgroud)