为什么不等待Task.WhenAll引发AggregateException?


101

在此代码中:

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


7
WhenAll 确实创建了一个AggregateException。如果您在示例中使用,Task.Wait而不是await在示例中,则可能会抓到AggregateException
Peter Ritchie 2012年

2
+1,这就是我要设法解决的问题,为我节省了调试和谷歌搜索的时间。
kennyzx 2012年

几年来,我第一次需要获得来自的所有例外Task.WhenAll,而我陷入了同一个陷阱。因此,我尝试深入了解此行为。
noseratio

Answers:


75

我不完全记得在哪里,但是我在某处读到了新的async / await关键字,它们将展开AggregateException为实际的异常。

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

为了将现有代码更轻松地转换为使用异步/等待,许多代码需要特定的异常而不是聚集的异常,这也是需要的。

-编辑-

得到它了:

比尔·瓦格纳的异步入门

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

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


3
是的,我知道对异常处理进行了一些更改,但是Task.WhenAll状态的最新文档“如果任何提供的任务以故障状态完成,则返回的任务也将以故障状态完成,其中其异常将包含从每个提供的任务中获取的未包装的异常集合的集合“。...以我为例,我的两个任务都在故障状态下完成...
Michael Ray Lovett

4
@MichaelRayLovett:您不会将返回的Task存储在任何地方。我敢打赌,当您查看该任务的Exception属性时,将得到一个AggregateException。但是,在您的代码中,您正在使用await。这样就可以将AggregateException展开为实际的异常。
decyclone

3
我也想到了这一点,但出现了两个问题:1)我似乎无法弄清楚如何存储任务,因此可以对其进行检查(即“ Task myTask = await Task.WhenAll(...)”似乎没有用。2)我想我看不到等待如何将多个异常表示为一个异常。它应该报告哪个异常?随机选择一个?
Michael Ray Lovett

2
是的,当我存储任务并在等待的try / catch中检查它时,我看到它的异常是AggregatedException。因此,我阅读的文档是正确的;Task.WhenAll将异常包装在AggregateException中。但是,等待正在解开它们。我在读你的文章了,但我看不出还怎么的await可以从AggregateExceptions挑一个异常并抛出一个对另一个..
迈克尔·雷·洛维特

3
阅读文章,谢谢。但是我仍然不明白为什么await将AggregateException(代表多个异常)表示为一个单独的异常。如何全面处理异常?..我猜如果我想确切地知道哪些任务引发了异常,哪些任务引发了异常,我将不得不检查Task.WhenAll创建的Task对象。
Michael Ray Lovett

55

我知道这是一个已经回答的问题,但是选择的答案并不能真正解决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");
}

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

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


出色的明确答案,应该由IMO选出。
bytedev

3
+1,但您不能简单地将它throw task.Exception;放在catch块中吗?(这使我在实际处理异常时看到一个空的陷阱。)
AnorZaken

@AnorZaken绝对;我不记得为什么我会这样写,但是我看不到任何缺点,所以我将其移到了catch块中。谢谢
Richiban

这种方法的一个小缺点是取消状态(Task.IsCanceled)无法正确传播。这可以使用扩展帮助像解决了这个
noseratio

34

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

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

2
这是行不通的。WhenAll在第一个异常退出并返回该异常。看到:stackoverflow.com/questions/6123406/waitall-vs-whenall
jenson-button-event

14
前两个注释不正确。该代码实际上可以工作,并且exceptions包含引发的两个异常。
Tobias

DoLongThingAsyncEx2()必须抛出新的InvalidOperationException()而不是新的InvalidOperation()
Artemious

8
为了缓解这里的疑问,我整理了一个扩展的小提琴,希望确切显示这种处理的效果:dotnetfiddle.net/X2AOvM。您可以看到await导致第一个异常被解开的原因,但是所有异常确实仍然可以通过Tasks数组获得。
Nuclearpidgeon

13

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

11

您正在考虑Task.WaitAll-它会引发AggregateException

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


3
这是错误的,从WhenAll方法返回的任务具有一个Exception属性,该属性AggregateException包含抛出其的所有异常InnerExceptions。这里发生的是await抛出第一个内部异常而不是AggregateException自身(例如decyclone所说)。调用任务的Wait方法而不是等待它会导致引发原始异常。
Şafak古尔

3

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

问题

等待task返回者Task.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]);
}

在这里,实例的AggregateExceptionget解开为它的第一个内部异常InvalidOperationException的方式与我们可能使用过的方式完全相同Task.WhenAll。我们可能没有观察到DivideByZeroException我们是否没有task.Exception.InnerExceptions直接经历。

微软的斯蒂芬·图布Stephen Toub)相关的GitHub问题中解释了此行为背后的原因:

我要提出的观点是,几年前(最初添加这些内容时)进行了深入讨论。我们最初是按照您的建议执行的,从WhenAll返回的Task包含一个包含所有异常的AggregateException,即task.Exception将返回一个AggregateException包装器,该包装器包含另一个AggregateException,该包装器随后包含实际的异常;然后在等待它时,将传播内部AggregateException。我们收到的强烈反馈导致我们更改了设计,原因是: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只保存唯一的内容以进行检查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}");
}

2
意想不到的答案

-3

这对我有用

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

1
WhenAll与相同WhenAnyawait Task.WhenAny(tasks)将在任何任务完成后立即完成。因此,如果您有一项任务立即完成并且成功完成,而另一项任务在引发异常之前需要花费几秒钟的时间,则该任务将立即返回而不会出现任何错误。
StriplingWarrior

然后,将永远不会在这里碰到投掷线-WhenAll会抛出异常
塞布(Thab)

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.