1. 介绍
想象一下:你同时启动十几个异步任务 — 爬网站、查电商价格或计算大数据。所有任务都嗖地启动了,但有几项失败了:有的服务器没响应,有的数据有问题。在同步代码里你通常只会看到一个异常,但在异步场景可能会有多个错误。这时候就轮到 AggregateException 上场了,它把所有错误装进一个“容器”里。
什么是 AggregateException?
AggregateException 是 .NET 里的一个特殊异常类型,用来聚合来自并行/异步操作的错误:比如 Task.WhenAll, Parallel.For, TPL 等。当两个或更多任务以错误结束时,它们会被“打包”进一个 AggregateException 对象。
在哪会出现 AggregateException?
最常见的场景是通过 Task.WhenAll 等待多个任务:
// 示例 — 并行运行多个任务,其中一些会抛出异常
var tasks = new List<Task>
{
Task.Run(() => throw new InvalidOperationException("错误 #1")),
Task.Run(() => throw new ArgumentException("错误 #2")),
Task.Run(() => { Console.WriteLine("任务3执行成功!"); })
};
try
{
await Task.WhenAll(tasks); // 在 await 这里 AggregateException 会被“展开”
}
catch (Exception ex)
{
Console.WriteLine($"发生错误: {ex.GetType().Name} - {ex.Message}");
}
但有个细节:在使用 await 时,AggregateException 会被“展开”,直接抛出第一个内部异常“原样”。如果不使用 await,而是访问 Result 或调用 Wait(),你会得到真正的 AggregateException。
2. AggregateException 的结构
这个异常包含一个集合 InnerExceptions — 所有的内层错误:
try
{
Task task = Task.WhenAll(tasks);
task.Wait(); // 如果有错误,这里会抛出 AggregateException
}
catch (AggregateException aggEx)
{
Console.WriteLine($"错误总数: {aggEx.InnerExceptions.Count}");
foreach (var e in aggEx.InnerExceptions)
{
Console.WriteLine($"类型: {e.GetType().Name}, 消息: {e.Message}");
}
}
示例输出:
错误总数: 2
类型: InvalidOperationException, 消息: 错误 #1
类型: ArgumentException, 消息: 错误 #2
AggregateException 是怎么产生的
+--------------------------+
| 启动多个 Task |
+-----+---------+----------+
| |
v v
Task1 Task2 ... TaskN
| |
| (出错)
|-------------------+
| v
| Exception2
v
Exception1
|
v
+---------------------------------+
| Task.WhenAll 或 Parallel.For |
| (收集所有异常) |
+---------------------------------+
|
v
AggregateException
(InnerExceptions: 所有错误)
为什么用 await 看不到 AggregateException?
在 await 时,.NET 会把 AggregateException “展开”,抛出第一个内部异常(InnerExceptions[0])。
// 只有一个抛出 InvalidOperationException 的任务
try
{
await Task.Run(() => throw new InvalidOperationException("出错!"));
}
catch (Exception ex)
{
// 这里 ex 是 InvalidOperationException,而不是 AggregateException
Console.WriteLine(ex.GetType().Name); // InvalidOperationException
}
想要访问所有错误的话——可以使用 Wait() 或 Result(但要记住会阻塞线程)。
3. 捕获并处理所有异常
把理论和实践联系起来。假设有几个任务在处理用户数据,你需要记录所有失败情况。
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace MyApp
{
class Program
{
static async Task Main(string[] args)
{
var tasks = new List<Task>
{
Task.Run(() => throw new InvalidOperationException("操作不正确!")),
Task.Run(() => throw new DivideByZeroException("除以零!")),
Task.Run(() => Console.WriteLine("第三个任务成功完成"))
};
try
{
// 异步等待任务 — 会得到第一个内部异常
Task allTasks = Task.WhenAll(tasks);
await allTasks; // 在 await 时 AggregateException 会被展开
}
catch (Exception ex)
{
// 在 await 时我们只会看到第一个内部异常
Console.WriteLine($"第一个错误: {ex.GetType().Name} - {ex.Message}");
// 如果这是 AggregateException — 可以遍历所有内部异常
if (ex is AggregateException aggEx)
{
foreach (var inner in aggEx.InnerExceptions)
{
Console.WriteLine($"列表中的错误: {inner.GetType().Name} - {inner.Message}");
}
}
}
// 现在同步地捕获所有错误
try
{
var allTasks = Task.WhenAll(tasks);
allTasks.Wait(); // 阻塞线程 — 在 UI 中别这样做!
}
catch (AggregateException aggEx)
{
foreach (var e in aggEx.InnerExceptions)
{
Console.WriteLine($"[Wait] 错误: {e.GetType().Name} — {e.Message}");
}
}
}
}
}
使用 await 在 try/catch 中你只会看到第一个错误。使用 Wait() 则会得到包含所有内部错误的传统 AggregateException。
4. 大规模错误处理
在真实应用中常常需要对大量错误进行不同处理。比如你给所有用户群发邮件,有一部分发送失败:
var sendTasks = emailList.Select(email => Task.Run(() => SendEmail(email))).ToList();
try
{
Task.WaitAll(sendTasks.ToArray());
}
catch (AggregateException aggEx)
{
foreach (var ex in aggEx.InnerExceptions)
{
if (ex is SmtpException)
Console.WriteLine("发送邮件错误: " + ex.Message);
else
Console.WriteLine("未知错误: " + ex.Message);
}
}
这样你就能记录每一次失败,并采取针对性的措施。
5. 有用的细节
这种行为的原因
.NET 的设计考虑的是方便性:在 UI 场景下通常第一条错误就够用了(在使用 await 时)。但在服务器或需要处理所有失败的系统里,AggregateException 是不可或缺的。
模式: Flatten — 展平错误列表
方法 Flatten() 会把嵌套的 AggregateException 展平成一个扁平的列表:
catch (AggregateException aggEx)
{
foreach (var ex in aggEx.Flatten().InnerExceptions)
{
Console.WriteLine($"展开后的错误: {ex.GetType().Name} — {ex.Message}");
}
}
不同等待方式下 Task 的行为
| 如何等待任务 | 会捕获到什么异常 |
|---|---|
|
第一个内部错误 |
|
总是 AggregateException |
|
总是 AggregateException |
| 单独的 await | 任务抛出的异常,或当任务以 AggregateException 结束时的第一个内部错误 |
需要记住
- AggregateException 在可能同时出现多个错误的场景里很有用。
- 对 await 来说抓第一个错误更方便;对同步等待来说遍历所有 InnerExceptions 更合适。
- 使用 Flatten() 和 Handle() 可以更优雅地处理。
- 不要在 UI 中混合同步和异步等待:会导致界面卡顿和死锁。
7. 处理 AggregateException 时常见的误区
误区:await Task.WhenAll 总是抛出 AggregateException。实际上不是,异常会被展开,你会看到第一个内部异常。
访问 Result 或调用 Wait() 会阻塞线程直到任务完成。在 UI 中这会造成界面卡顿。
如果你想“悄悄”处理一部分错误,可以使用 Handle():
catch (AggregateException aggEx)
{
aggEx.Handle(ex =>
{
if (ex is InvalidOperationException)
{
// 这个错误我们已经处理了,可以不再抛出
return true;
}
return false; // 其它异常会被重新抛出
});
}
GO TO FULL VERSION