【问题标题】:Handling multiple exceptions from async parallel tasks处理来自异步并行任务的多个异常
【发布时间】:2018-07-19 02:56:51
【问题描述】:

问题

多个任务并行运行,所有任务、无任务或其中任何任务都可能引发异常。当所有任务都完成后,必须报告所有可能发生的异常(通过日志、电子邮件、控制台输出......无论如何)。

预期行为

我可以通过带有异步 lambda 的 linq 构建所有任务,然后等待它们与 Task.WhenAll(tasks) 并行运行。然后我可以捕获AggregateException 并报告每个单独的内部异常。

实际行为

一个AggregateException 被抛出,但它只包含一个内部异常,无论抛出多少个单独的异常。

最小的完整可验证示例

static void Main(string[] args)
{
    try
    {
        ThrowSeveralExceptionsAsync(5).Wait();
    }
    catch (AggregateException ex)
    {
        ex.Handle(innerEx =>
        {
            Console.WriteLine($"\"{innerEx.Message}\" was thrown");
            return true;
        });
    }

    Console.ReadLine();
}

private static async Task ThrowSeveralExceptionsAsync(int nExceptions)
{
    var tasks = Enumerable.Range(0, nExceptions)
        .Select(async n =>
        {
            await ThrowAsync(new Exception($"Exception #{n}"));
        });

    await Task.WhenAll(tasks);
}

private static async Task ThrowAsync(Exception ex)
{
    await Task.Run(() => {
        Console.WriteLine($"I am going to throw \"{ex.Message}\"");
        throw ex;
    });
}

输出

请注意,由于竞争条件,“我要扔”消息的输出顺序可能会发生变化。

I am going to throw "Exception #0"
I am going to throw "Exception #1"
I am going to throw "Exception #2"
I am going to throw "Exception #3"
I am going to throw "Exception #4"
"Exception #0" was thrown

【问题讨论】:

  • 排序相关,你可以让你的入口点异步,只需添加static async Task Main
  • @BradM 如果你这样做,它会完全中断(可能是由于async Main 在内部的工作方式)
  • 有趣,不知道这些。不过,这对于另一个问题来说很重要。

标签: c# exception-handling parallel-processing async-await


【解决方案1】:

这是因为await“解包”聚合异常并且总是只抛出第一个异常(如await 的文档中所述),即使您在等待Task.WhenAll 时也是如此,这显然会导致多个错误。您可以像这样访问聚合异常:

var whenAll = Task.WhenAll(tasks);
try {
    await whenAll;
}
catch  {
    // this is `AggregateException`
    throw whenAll.Exception;
}

或者您可以循环访问任务并检查每个任务的状态和异常。

请注意,在修复之后,您还需要做一件事:

try {
    ThrowSeveralExceptionsAsync(5).Wait();
}
catch (AggregateException ex) {
    // flatten, unwrapping all inner aggregate exceptions
    ex.Flatten().Handle(innerEx => {
        Console.WriteLine($"\"{innerEx.Message}\" was thrown");
        return true;
    });
}

因为ThrowSeveralExceptionsAsync 返回的任务包含我们抛出的AggregateException,包裹在另一个AggregateException 中。

【讨论】:

  • 在原来的AggregateException 上调用Handle() 和在“扁平化”上调用它有什么区别?
  • @isaac 如果你不把它展平——在这种情况下,Handle 中的innerEx 将是另一个AggregateException(来自Task.WhenAll),这有点没用。 Flatten 展开所有内部聚合异常。
  • @Evk 谢谢,这解决了问题。您是否碰巧知道这是否记录在某处?这对我来说真的很不直观,并且每当我期望这种行为感觉很奇怪时,必须实现这种 try-catch-throw 模式。我想知道这个设计背后的原因(我相信他们是很好的理由)。
  • @DanielGarcíaRubio 是的,它在 await 关键字(例外部分)的文档中进行了描述:docs.microsoft.com/en-us/dotnet/csharp/language-reference/…。下面是 MS 员工为什么他们这样设计 await 的原因:social.msdn.microsoft.com/Forums/en-US/…。如果您经常等待Task.WhenAll - 您可以创建扩展方法并在那里提取重复逻辑。
  • 谢谢。与往常一样,理由是好的,但不直观。我想每当我需要运行并行任务并报告所有异常时,我都应该实现这种模式并记录该方法将始终抛出AggregateException,而不是WhateverExceptionThatMightHappenFirst。再说一次,如果您等待这样的方法,您将捕获嵌套的AggregateExceptions...
猜你喜欢
  • 1970-01-01
  • 2013-02-13
  • 1970-01-01
  • 1970-01-01
  • 2011-01-15
  • 1970-01-01
  • 1970-01-01
  • 2019-08-20
  • 1970-01-01
相关资源
最近更新 更多