处理异步并行任务的多个异常
Handling multiple exceptions from async parallel tasks
问题
多个任务 运行 并行,所有 none 或其中任何一个都可能抛出异常。当所有任务完成后,必须报告所有可能发生的异常(通过日志、电子邮件、控制台输出......等等)。
预期行为
我可以通过带有异步 lambda 的 linq 构建所有任务,然后等待它们 运行ning 与 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" 消息的输出顺序可能会因竞争条件而改变。
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
那是因为 await
"unwraps" 聚合异常并且总是抛出第一个异常(如 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
.
中
问题
多个任务 运行 并行,所有 none 或其中任何一个都可能抛出异常。当所有任务完成后,必须报告所有可能发生的异常(通过日志、电子邮件、控制台输出......等等)。
预期行为
我可以通过带有异步 lambda 的 linq 构建所有任务,然后等待它们 运行ning 与 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" 消息的输出顺序可能会因竞争条件而改变。
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
那是因为 await
"unwraps" 聚合异常并且总是抛出第一个异常(如 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
.