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

Posted

技术标签:

【中文标题】我想等待抛出 AggregateException,而不仅仅是第一个异常【英文标题】:I want await to throw AggregateException, not just the first Exception 【发布时间】:2013-08-21 07:38:29 【问题描述】:

当等待一个出错的任务(一个设置了异常的任务)时,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 上的其他问题并未解决此特定问题。建议重复时请小心。

【问题讨论】:

您是否看到这篇博客文章解释了他们为什么选择像他们一样为await 实施异常处理? blogs.msdn.com/b/pfxteam/archive/2011/09/28/10217876.aspx 好的,谢谢。对于一般情况,我有点同意这种推理,但我的用例没有被涵盖。 “在所有情况下,Task 的 Exception 属性仍然返回包含所有异常的 AggregateException,因此您可以捕获抛出的任何异常,并在需要时返回查看 Task.Exception。” 相关GitHub API提案:Configure an await to propagate all errors by throwing an AggregateException 这是一个保留所有异常并正确传播取消状态的解决方案:***.com/a/62607500/1768303 【参考方案1】:

我不同意你的问题标题中暗示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();
  

【讨论】:

获取所有异常对于诊断很重要。我不想错过 nullrefs 等等。他们必须被记录。 处理 异常是另一种情况。顺便说一句,该解决方案工作正常。谢谢。 我基本同意。我想到的场景确实是一个***错误处理程序(它所做的只是记录崩溃)。但是完整的崩溃信息必须以某种方式到达那里。 UnobservedTaskException 和 Application 处理程序不保证在 await 存在的情况下,因为错误可能最终被观察到但被吞没。当我看到错误被吞下时,我会非常紧张。我见过太多的案例,其中一个月来生产出现严重错误而没有人注意到。 UnobservedTaskException is still raised 用于未观察到的异常(它只是不再使进程崩溃)。只有WhenAll 观察到了被吞下的异常,并且只有当您有多个异常时才会发生这种情况,其中一个没有被吞下。您确实必须尝试忽略await 的异常。 考虑到source.Exception 在取消期间可能是空引用。【参考方案2】:

我知道我迟到了,但我发现了这个巧妙的小技巧,它可以满足你的需求。由于 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); );
    

【讨论】:

【参考方案3】:

这里是 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 中的this API 提案中的建议。


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

【讨论】:

【参考方案4】:

Exception Handling (Task Parallel Library)

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

也许你想要这个

God (Jon Skeet) explains await exception handling

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

回复 cmets(评论回复太长)

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

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

不要混淆您的用例。

【讨论】:

第一个链接不处理await。第二个包含一个有效的答案。谢谢。我会在几天内让其他答案出现,因为我想发现最佳解决方案。 问题是我不同意你的用例。我认为,当您使用 await 时,您假设每个任务都处理自己的异常 - await 告诉您在无法取消其他任务的操作中出现问题(即它们必须是独立的)。或者至少我认为这是在它的模式中假设的。如果你想处理任务之外的异常,那么你应该使用顶部模式。我可能应该解释一下。此外,如果您想要聚合异常 - 问题 - 第一个应该是您的选择。 ***模式涉及阻塞,这是一个与等待地址完全不同的用例。一个有效的用例,但不是我的——我在这里需要异步 IO 和非阻塞。此外,任务不一定会处理自己的错误。如果是这种情况,我们将永远不需要错误传播。 应该传播错误,从而中止正常的控制流程。我想要的只是 all 错误,而不仅仅是任意错误。 更新了 ... ofc 任务处理自己的错误 ... 任务它是围绕工作单元的封装单元。我认为处理其异常的工作是其中最明确的一部分,否则你正在为自己创造一个锁定噩梦,因为 100 个线程都抛出异常并阻塞一段代码 - 除非你让它在这一点上没有状态你会意识到它本质上无论如何都存在于任务本身中......或者与我妥协并传入一个 lambda 来处理异常。 @JohnNicholas 对上帝的文章的引用被破坏了(【参考方案5】:

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

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 包裹有助于不丢失原始堆栈跟踪。

【讨论】:

【参考方案6】:

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

 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

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

【讨论】:

ContinueWith 是一个primitive 方法,使用时应格外小心,尤其是与 async/await 代码结合使用时。还有t被取消的情况如何? UnswallowExceptions 方法如何处理这种情况? 它显然会抛出。我将使用此信息更新答案 我不认为它会抛出。它可能会返回一个永远无法转换到Canceled 状态的任务。顺便说一句,没有测试你的方法,我认为它应该与这个较短的版本类似:=&gt; t.ContinueWith(t =&gt; t.Result); 我在更新答案之前进行了测试,它在访问 t.Result 时会抛出 TaskCanceledException,并且它被包裹在 AggregationException 中,因此您会丢失原始异常并捕获 AggregationException,其中只有 TaskCanceledException。可以在访问 t.Result 之前添加对 t.IsCanceled 的检查,但随后您会得到错误的任务结果而不是取消任务。如果你需要所有这些,你最好检查一下 John Skeet 的解决方案,它涵盖了所有内容,但在每次调用时创建 TaskCompletionSource 对我来说都是多余的 为什么不使用 Stephen Cleary 的 solution?它很短,似乎可以正确处理所有情况。你想出于某种原因避免 async/await 吗?

以上是关于我想等待抛出 AggregateException,而不仅仅是第一个异常的主要内容,如果未能解决你的问题,请参考以下文章

如何在不抛出 TaskCanceledExceptions 的情况下等待任务?

如何修复mscorlib.dll中发生的''System.AggregateException'“

Nuget 包 - 提要 (VSTS):尝试添加源时抛出异常“System.AggregateException”

等待具有 OnlyOnFaulted Continuation 的任务会导致 AggregateException

C# 使用AggregateException 信息

Web Api 的 PostAsync HttpClient 错误 - System.AggregateException“任务已取消。”