ChatGPT解决这个技术问题 Extra ChatGPT

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

在这段代码中:

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 关于这种行为。
@PeterRitchie 这是真的,但请注意 Task.Wait 正在阻塞,而 await 不是。

R
Richiban

我知道这是一个已经回答的问题,但选择的答案并不能真正解决 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<int> 这有效(Task.WhenAll() 将返回 Task<int[]>)。如果 AB 返回不同的类型,或者其中至少有一个是 void,那么您是正确的,var results = await task 将不起作用。
C
Community

我不完全记得在哪里,但我在某个地方读到了新的 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 方法的另一个原因....


是的,我知道异常处理发生了一些变化,但是 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 对象?
t
themefield

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

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

这里有很多好的答案,但我仍然想发表我的咆哮,因为我刚刚遇到了同样的问题并进行了一些研究。或跳至下方的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 Toubthe 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 变量。
@TheodorZoulias,想法!我只是想要一个单行来介绍 ex :)
我认为使用 this 之类的 try { await source.ConfigureAwait(false); } catch { source.Wait(); } 时不会正确传播 Task.IsCanceled。我认为它会变成Task.IsFaulted。也许不是什么大问题,但值得关注。
顺便说一句,AFAIK 异步方法永远不会产生具有(最终)取消状态的任务 - 不确定我是否遵循,但这个可以:async Task TestAsync() { await Task.FromException(new TaskCanceledException()); }Task.IsCanceled 将在此处为 true,就像我们刚刚在 async method 中执行 throw new TaskCanceledException() 一样。
D
Daniel Šmon

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

J
Just code

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

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


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

你真正需要做的是:

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

A
Alexey Kulikov

这对我有用

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 行 - WhenAll 会抛出异常
N
Nebula

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