我想等待抛出AggregateException,而不仅仅是第一个Exception

时间:2013-08-19 13:25:34

标签: c# task-parallel-library c#-5.0 async-await

等待出现故障的任务(具有异常集的任务)时,await将重新抛出存储的异常。如果存储的异常是AggregateException,它将重新抛出第一个并丢弃其余的异常。

我们如何使用await并同时抛出原始AggregateException,以便我们不会意外丢失错误信息?

注意,当然可以考虑使用hacky解决方案(例如await周围的试试,然后调用Task.Wait)。我真的希望找到一个干净的解决方案。 这里的最佳做法是什么?

我想过使用自定义等待,但内置的TaskAwaiter包含许多魔法,我不知道如何完全重现。它调用TPL类型的内部API。我也不想要重现所有这些。

如果你想玩它,这是一个简短的复制品:

static void Main()
{
    Run().Wait();
}

static async Task Run()
{
    Task[] tasks = new[] { CreateTask("ex1"), CreateTask("ex2") };
    await Task.WhenAll(tasks);
}

static Task CreateTask(string message)
{
    return Task.Factory.StartNew(() => { throw new Exception(message); });
}

Run中仅抛出两个例外中的一个。

请注意,Stack Overflow上的其他问题无法解决此特定问题。建议重复时请小心。

6 个答案:

答案 0 :(得分:21)

我不同意你的问题标题中暗示await的行为是不受欢迎的。在绝大多数场景中都有意义。在WhenAll情况下,您真正多久需要知道所有错误详细信息,而不仅仅是一个?

AggregateException的主要困难是异常处理,即你失去了捕获特定类型的能力。

也就是说,您可以使用扩展方法获得所需的行为:

public static async Task WithAggregateException(this Task source)
{
  try
  {
    await source.ConfigureAwait(false);
  }
  catch
  {
    // source.Exception may be null if the task was canceled.
    if (source.Exception == null)
      throw;

    // EDI preserves the original exception's stack trace, if any.
    ExceptionDispatchInfo.Capture(source.Exception).Throw();
  }
}

答案 1 :(得分:4)

我知道我迟到但我发现了这个巧妙的小技巧,可以做你想要的。由于等待任务可以使用完整的异常集,因此调用此Task的Wait或.Result将抛出一个聚合异常。

create_time

答案 2 :(得分:3)

Exception Handling (Task Parallel Library)

我可以说更多,但它只是填充。玩它,它确实像他们说的那样工作。你必须要小心。

也许你想要这个

God (Jon Skeet) explains await exception handling

(我个人躲避等待,但这只是我的偏好)

回应评论(评论回复太长)

然后使用线程作为类似参数的起点,因为最佳实践将是这里的源代码。

除非你实现代码将它们传递出去,否则很快就会被吞噬掉(例如,await可能包装的异步模式......你在引发事件时将它们添加到事件args对象中)。如果您有一个场景,您可以启动任意数量的线程并对其执行,则无法控制订单或终止每个线程的点。此外,如果一个错误与另一个错误相关,您将永远不会使用此模式。因此,你强烈暗示其余的执行是完全独立的 - IE你强烈暗示这些线程上的异常已经作为异常处理。如果你想在它们出现的线程(这是bizzarre)中处理这些线程中的异常以外的事情,你应该将它们添加到通过引用传入的锁定集合中 - 你不再将异常视为例外而是作为一块信息 - 使用并发包,将异常包装在您需要的信息中以识别它来自的上下文 - 这将被传递到它中。

不要混淆您的使用案例。

答案 3 :(得分:1)

我不想放弃只捕获我期望的异常的做法。这将导致我使用以下扩展方法:

public static async Task NoSwallow<TException>(this Task task) where TException : Exception {
    try {
        await task;
    } catch (TException) {
        var unexpectedEx = task.Exception
                               .Flatten()
                               .InnerExceptions
                               .FirstOrDefault(ex => !(ex is TException));
        if (unexpectedEx != null) {
            throw new NotImplementedException(null, unexpectedEx);
        } else {
            throw task.Exception;
        }
    }
}

消费代码可能像这样:

try {
    await Task.WhenAll(tasks).NoSwallow<MyException>();
catch (AggregateException ex) {
    HandleExceptions(ex);
}

骨头异常将具有与同步世界相同的效果,即使偶然将其与MyException同时抛出也是如此。 NotImplementedException换行有助于避免丢失原始堆栈跟踪。

答案 4 :(得分:1)

我不知道是否可以总结这个非常有趣的问题中提出的问题的两种最佳解决方案(我认为),史蒂芬•克利迪先生阿登西先生。我通过添加参数preserveAggregate来稍微更改了API,以尝试与单个内置配置选项ConfigureAwait保持一致。这些配置是可链接的,例如:

await Task.WhenAll(tasks).ConfigureException(true).ConfigureAwait(false);

这是Stephen Cleary's先生的版本(略有修改):

public static class TaskConfigurationExtensions
{
    public static async Task ConfigureException(this Task task, bool preserveAggregate)
    {
        try
        {
            await task.ConfigureAwait(false); // Because the context doesn't have to be resumed on to throw.
        }
        catch
        {
            if (preserveAggregate) throw task.Exception;
            throw;
        }
    }
    public static async Task<T> ConfigureException<T>(this Task<T> task, bool preserveAggregate)
    {
        try
        {
            return await task.ConfigureAwait(false);
        }
        catch
        {
            if (preserveAggregate) throw task.Exception;
            throw;
        }
    }
}

这是adonthy's先生的版本(也略有修改):

public static class TaskConfigurationExtensions
{
    private static void Empty<T>(T value) { }
    public static async Task ConfigureException(this Task task, bool preserveAggregate)
    {
        if (preserveAggregate)
        {
            await task
                .ContinueWith(Empty, TaskContinuationOptions.ExecuteSynchronously)
                .ConfigureAwait(false);
            task.Wait();
            return;
        }
        await task.ConfigureAwait(false);
    }
    public static async Task<T> ConfigureException<T>(this Task<T> task, bool preserveAggregate)
    {
        if (preserveAggregate)
        {
            await task
                .ContinueWith(Empty, TaskContinuationOptions.ExecuteSynchronously)
                .ConfigureAwait(false);
            return task.Result;
        }
        return await task.ConfigureAwait(false);
    }
}

据我所知,这两种实现都是等效且出色的。

答案 5 :(得分:0)

包装原始聚合异常并且不改变返回类型的扩展,因此它仍然可以与Task<T>一起使用

 public static Task<T> UnswallowExceptions<T>(this Task<T> t)
        => t.ContinueWith(t => t.IsFaulted ? throw new AggregateException("whatever", t.Exception) : t.Result);

示例:

Task<T[]> RunTasks(Task<T>[] tasks) => 
Task.WhenAll(CreateSometasks()).UnswallowExceptions();
try
{ var result = await CreateTasks(); }
catch(AggregateException ex) {  } //ex is original aggregation exception here

注意如果任务被取消,此方法将抛出,如果取消对您很重要,请使用另一种方法