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

时间:2012-08-17 14:33:25

标签: .net exception asynchronous 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();});
}

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

WhenAll并不总是会创建AggregateException吗?

9 个答案:

答案 0 :(得分:58)

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

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

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

- 编辑 -

知道了:

An Async Primer by Bill Wagner

  比尔瓦格纳说:(在当例外情况发生时

     

...当你使用await时,编译器生成的代码会解开   AggregateException并抛出基础异常。通过杠杆化   等待,您可以避免额外的工作来处理AggregateException类型   由Task.Result,Task.Wait和其他定义的Wait方法使用   任务类。这是使用等待而不是使用等待的另一个原因   基础任务方法......

答案 1 :(得分:24)

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

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

答案 2 :(得分:23)

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

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

答案 3 :(得分:12)

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

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

答案 4 :(得分:9)

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

问题

等待task返回的Task.WhenAll只会抛出AggregateException中存储的task.Exception的第一个异常,即使有多个任务出错。

current docs for 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 Toubthe related GitHub issue中解释了此行为背后的原因:

我要说的是,它已经被深入讨论, 多年前,最初添加这些内容时。我们最初做了什么 您的建议是,WhenWhen返回的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}");
}

答案 5 :(得分:6)

我想我会扩展@ Richiban的答案,说你也可以通过从任务中引用它来处理catch块中的AggregateException。 E.g:

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 :(得分:-3)

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

答案 7 :(得分:-3)

这对我有用

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

答案 8 :(得分:-5)

试试这个代码模式:

Task task = null;
try
{
    task = Task.WhenAll(...);
    await task;
 }
 catch (AggregateException aggException)
 {
     var aggException = task.Exception;
     ...
 }