21

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

我们怎样才能使用await并同时扔掉原来的AggregateException东西,这样我们才不会不小心丢失错误信息?

请注意,当然可以为此考虑一些 hacky 解决方案(例如 try-catch 周围的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 的其他问题并未解决此特定问题。建议重复时请小心。

4

6 回答 6

24

我不同意您的问题标题中暗示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();
  }
}
于 2013-08-19T13:56:25.237 回答
5

我知道我迟到了,但我发现这个巧妙的小技巧可以满足你的要求。由于 on awaited Task 提供了完整的异常集,因此调用此 Task 的 Wait 或 .Result 将引发聚合异常。

    static void Main(string[] args)
    {
        var task = Run();
        task.Wait();
    }
    public static async Task Run()
    {

        Task[] tasks = new[] { CreateTask("ex1"), CreateTask("ex2") };
        var compositeTask = Task.WhenAll(tasks);
        try
        {
            await compositeTask.ContinueWith((antecedant) => { }, TaskContinuationOptions.ExecuteSynchronously);
            compositeTask.Wait();
        }
        catch (AggregateException aex)
        {
            foreach (var ex in aex.InnerExceptions)
            {
                Console.WriteLine(ex.Message);
            }
        }
    }

    static Task CreateTask(string message)
    {
        return Task.Factory.StartNew(() => { throw new Exception(message); });
    }
于 2016-12-31T17:43:21.543 回答
4

这是 Stephen Cleary 的WithAggregateException扩展方法的一个较短的实现:

public static async Task WithAggregateException(this Task source)
{
    try { await source.ConfigureAwait(false); }
    catch (OperationCanceledException) when (source.IsCanceled) { throw; }
    catch { source.Wait(); }
}

public static async Task<T> WithAggregateException<T>(this Task<T> source)
{
    try { return await source.ConfigureAwait(false); }
    catch (OperationCanceledException) when (source.IsCanceled) { throw; }
    catch { return source.Result; }
}

此方法基于 Stephen Toub在 GitHub 中的API 提案中的建议。


更新:我添加了对取消情况的特殊处理,以防止传播AggregateException包含OperationCanceledException. 现在OperationCanceledException直接传播,并Task.IsCanceled保留状态。感谢@noseratio 在这个答案的评论中指出了这个缺陷。当然,现在这个实现并不比 Stephen Cleary 的方法短很多!

于 2019-04-13T09:30:50.507 回答
3

异常处理(任务并行库)

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

也许你想要这个

上帝(Jon Skeet)解释等待异常处理

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

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

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

除非您实现将异常传递出去的代码(例如,等待可能包装的异步模式......您在引发事件时将它们添加到事件 args 对象中),否则异常会很高兴地被吞没。当您启动任意数量的线程并在它们上执行时,您无法控制顺序或终止每个线程的点。此外,如果一个错误与另一个相关,您将永远不会使用此模式。因此,您强烈暗示其余部分的执行是完全独立的 - 即您强烈暗示这些线程上的异常已作为异常处理。如果您想做一些事情,而不是在它们发生的线程中处理这些线程中的异常(这很奇怪),您应该将它们添加到通过引用传入的锁定集合中 - 您不再将异常视为异常,而是将其视为一个部分信息 - 使用并发包,将异常包装在您需要识别它来自的上下文的信息中 - 这将被传递给它。

不要混淆你的用例。

于 2013-08-19T13:50:22.020 回答
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有助于不丢失原始堆栈跟踪。

于 2018-10-01T09:50:55.500 回答
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

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

于 2021-03-10T15:43:53.183 回答