C# 处理异步并行任务中的多个异常

C# 处理异步并行任务中的多个异常,c#,exception-handling,parallel-processing,async-await,C#,Exception Handling,Parallel Processing,Async Await,问题 多个任务并行运行,所有、无或任何任务都可能引发异常。当所有任务完成后,必须报告可能发生的所有异常(通过日志、电子邮件、控制台输出…无论什么) 预期行为 我可以使用异步lambdas通过linq构建所有任务,然后等待它们与Task.whalll(tasks)并行运行。然后我可以捕获一个aggregateeexception,并报告每个单独的内部异常 实际行为 抛出一个AggregateException,但它只包含一个内部异常,不管抛出的单个异常的数量是多少 最小完整可验证示例 static

问题

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

预期行为

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

实际行为

抛出一个
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

这是因为,即使在等待任务时,
wait
“展开”聚合异常并始终只引发第一个异常(如的文档中所述)。whalll,这显然会导致多个错误。您可以访问聚合异常,例如:

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

由于通过EverAlexceptionAsync返回的任务包含我们抛出的、包装在另一个
AggregateException

排序相关的
AggregateException
中的
AggregateException
任务,您可以使入口点异步,只需添加
static async task Main
@BradM即可。如果这样做,它将完全中断(可能是因为
async Main
在内部的工作方式)很有趣,我不知道这其中的任何一个。但这对于另一个问题来说是很重要的。在原始
AggregateException
上调用
Handle()
和在“展平”上调用它有什么区别一个?@isaac如果你不展平它-
Handle
中的
innerEx
,在这种情况下将是另一个
aggregateeexception
(一个来自
Task.whalll
),这有点没用。展平打开所有内部聚合异常。@Evk谢谢,这解决了问题。你知道这是否有文档记录吗?这对我来说真的很不直观,每当我认为这种行为感觉奇怪时,就必须实现这种尝试-捕获-抛出模式。我想知道这种设计背后的原因gn(我相信这是很好的理由)。@DanielGarcíaRubio是的,它在
wait
关键字的文档中有描述(部分例外):。下面是微软员工设计Wait的原因:。如果您经常等待
任务。当所有
任务完成时,您可以在那里创建扩展方法并提取重复逻辑。谢谢。一如既往,原因很好,但不直观。我想,每当我需要运行并行任务并报告所有异常时,我都应该实现此模式和文档表明,该方法将始终抛出
aggregateeexception
,而不是
whateexception,它可能首先发生
。然后,如果您等待这样的方法,您将捕获嵌套的
aggregateeexception
。。。