马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?立即注册
×
07. 异步非常处理处罚:AggregateException 的拆解与最佳实践
本章 GitHub 堆栈:csharp-concurrency-cookbook ⭐
欢迎 Star 和 Fork!全部代码示例都可以在堆栈中找到并运行。
🎯 本章导读
📌 本文目的:把握异步非常处理处罚的精确姿势,明白 AggregateException 的筹划头脑,学会在复杂并发场景下优雅处理处罚非常。
你是否遇到过如许的场景:
- 为什么 await 抛出的是单个非常,而 .Result 抛出的是 AggregateException?
- 同时调用 10 个 API,此中 3 个失败了,怎样获取全部失败信息?
- Task.WhenAll 抛非常时,为什么只能捕捉第一个?
- 配景使命(Fire-and-forget)的非常去哪儿了?
- 怎样实现一个"容错"的并发使命实验器?
本日,我们就来彻底搞懂 .NET 异步编程中的非常处理处罚机制,从 AggregateException 的筹划理念到实战本领,一扫而空。
⚠️ 告急提示:本文涉及异步编程的核心概念,发起先把握前面章节的 Task、async/await 和 CancellationToken 根本。
0️⃣ 一个真实的故事:消散的非常
0.1 场景重现:批量调用 API
假设你正在写一个数据同步工具,必要同时调用 10 个微服务的 API,获取数据并汇总。
你写出了第一版代码:- public async Task<List<UserData>> GetAllUsersAsync()
- {
- var tasks = new List<Task<UserData>>();
- // 并发调用 10 个 API
- for (int i = 1; i <= 10; i++)
- {
- tasks.Add(GetUserDataAsync(i));
- }
- // 等待所有任务完成
- var results = await Task.WhenAll(tasks);
- return results.ToList();
- }
- private async Task<UserData> GetUserDataAsync(int userId)
- {
- using var client = new HttpClient();
- var response = await client.GetStringAsync($"https://api.example.com/users/{userId}");
- return JsonSerializer.Deserialize<UserData>(response);
- }
复制代码 标题:
- 三个使命同时实验,都失败了
- 传统的非常机制只能抛出一个非常
- 假如只抛出第一个,其他两个非常信息就丢失了
办理方案:AggregateException——一个可以包罗多个非常的容器。
1.2 AggregateException 的筹划结构
- 未处理的异常: System.Net.Http.HttpRequestException: Response status code does not indicate success: 404 (Not Found).
复制代码 核心特性:
- InnerExceptions:存储全部子使命的非常
- Flatten():处理处罚嵌套的 AggregateException
- Handle():选择性处理处罚某些非常,未处理处罚的会重新抛出
示例:- try
- {
- var results = await Task.WhenAll(tasks);
- }
- catch (Exception ex)
- {
- Console.WriteLine($"出错了: {ex.Message}");
- // ❌ 只能捕获第一个异常!其他失败的任务信息丢失
- }
复制代码 输出:- try
- {
- var whenAllTask = Task.WhenAll(tasks);
- whenAllTask.Wait(); // 或者 .Result
- }
- catch (AggregateException aggEx)
- {
- foreach (var ex in aggEx.InnerExceptions)
- {
- Console.WriteLine($"出错了: {ex.Message}");
- }
- // ✅ 可以获取所有异常,但 Wait() 会阻塞线程!
- }
复制代码 1.3 await vs Wait/Result 的非常活动差异
这是一个非常告急的知识点,许多开辟者在这里踩坑。
场景:单个使命失败
- var whenAllTask = Task.WhenAll(tasks);
- try
- {
- await whenAllTask;
- }
- catch (Exception firstEx)
- {
- // 捕获第一个异常
- Console.WriteLine($"第一个异常: {firstEx.Message}");
- // 如果需要所有异常,从 Task.Exception 中获取
- if (whenAllTask.Exception != null)
- {
- Console.WriteLine("\n所有异常:");
- foreach (var ex in whenAllTask.Exception.InnerExceptions)
- {
- Console.WriteLine($"- {ex.Message}");
- }
- }
- }
复制代码 方式 1:使用 await(保举)- public void ProcessData()
- {
- ValidateInput(); // 可能抛出 ArgumentException
- ConnectDatabase(); // 可能抛出 SqlException
- SaveData(); // 可能抛出 IOException
- // ❌ 一旦抛出异常,后面的代码不会执行
- }
复制代码 方式 2:使用 Wait() 或 .Result- var task1 = Task.Run(() => throw new InvalidOperationException("Task 1 failed"));
- var task2 = Task.Run(() => throw new ArgumentException("Task 2 failed"));
- var task3 = Task.Run(() => throw new IOException("Task 3 failed"));
- await Task.WhenAll(task1, task2, task3);
- // ❓ 三个任务都失败了,应该抛出哪个异常?
复制代码 对比表格:
特性await.Wait() / .Result抛出的非常范例原始非常(第一个)AggregateException获取原始非常直接捕捉aggEx.InnerException多个非常只抛出第一个InnerExceptions 包罗全部线程壅闭❌ 不壅闭✅ 壅闭当火线程死锁风险✅ 安全❌ 大概死锁(UI 线程)保举使用✅ 剧烈保举❌ 只管制止结论:
- 优先使用 await:代码更轻便,非常处理处罚更直观
- 必要全部非常:通过 Task.Exception 属性获取 AggregateException
- 制止使用 .Wait() 和 .Result:会壅闭线程,大概导致死锁
2️⃣ Task.WhenAll 的非常陷阱与办理方案
2.1 标题:WhenAll 只抛出第一个非常
这是 Task.WhenAll 最轻易踩坑的地方。
示例:- public class AggregateException : Exception
- {
- // 存储所有内部异常
- public ReadOnlyCollection<Exception> InnerExceptions { get; }
- // 扁平化嵌套的 AggregateException
- public AggregateException Flatten();
- // 按条件处理异常
- public void Handle(Func<Exception, bool> predicate);
- }
复制代码 输出:- try
- {
- var task1 = Task.Run(() => throw new InvalidOperationException("Task 1 failed"));
- var task2 = Task.Run(() => throw new ArgumentException("Task 2 failed"));
- var task3 = Task.Run(() => throw new IOException("Task 3 failed"));
- Task.WaitAll(task1, task2, task3); // ❌ 同步等待,会抛出 AggregateException
- }
- catch (AggregateException aggEx)
- {
- Console.WriteLine($"捕获了 {aggEx.InnerExceptions.Count} 个异常:");
- foreach (var ex in aggEx.InnerExceptions)
- {
- Console.WriteLine($"- {ex.GetType().Name}: {ex.Message}");
- }
- }
复制代码 标题:只看到了第一个非常,其他失败信息丢失了!
2.2 办理方案 1:手动查抄 Task.Exception
- 捕获了 3 个异常:
- - InvalidOperationException: Task 1 failed
- - ArgumentException: Task 2 failed
- - IOException: Task 3 failed
复制代码 输出:- var task = Task.Run(() => throw new InvalidOperationException("Something went wrong"));
复制代码 优点:✅ 可以获取全部非常信息
缺点:❌ 代码略显冗长
2.3 办理方案 2:逐个 await(更轻便)
- try
- {
- await task;
- }
- catch (InvalidOperationException ex)
- {
- Console.WriteLine($"捕获异常: {ex.Message}");
- // ✅ 直接抛出原始异常,简化异常处理
- }
复制代码 输出:- try
- {
- task.Wait(); // 或者 var result = task.Result;
- }
- catch (AggregateException aggEx)
- {
- // ❌ 包装在 AggregateException 中,需要额外解包
- var innerEx = aggEx.InnerException;
- Console.WriteLine($"捕获异常: {innerEx.Message}");
- }
复制代码 优点:
- ✅ 可以单独处理处罚每个使命的非常
- ✅ 代码清楚,逻辑直观
注意:
- Task.WhenAll 仍旧必要调用,确保全部使命并发实验
- 逐个 await 时,已完成的使命会立刻返回,不会重新实验
2.4 办理方案 3:实现 SafeWhenAll 扩展方法(最优雅)
目的:封装非常处理处罚逻辑,返回乐成和失败的效果。- public async Task CallMultipleApisAsync()
- {
- var tasks = new[]
- {
- CallApiAsync(1), // ✅ 成功
- CallApiAsync(2), // ❌ 失败:404 Not Found
- CallApiAsync(3), // ✅ 成功
- CallApiAsync(4), // ❌ 失败:500 Internal Server Error
- CallApiAsync(5), // ❌ 失败:Timeout
- };
- try
- {
- await Task.WhenAll(tasks);
- }
- catch (Exception ex)
- {
- Console.WriteLine($"捕获异常: {ex.Message}");
- // ❌ 只能看到第一个失败的异常(404 Not Found)
- // ❌ 其他两个失败(500 和 Timeout)的信息丢失
- }
- }
复制代码 使用示例:- 捕获异常: Response status code does not indicate success: 404 (Not Found).
复制代码 输出:- public async Task CallMultipleApisAsync()
- {
- var tasks = new[]
- {
- CallApiAsync(1),
- CallApiAsync(2),
- CallApiAsync(3),
- CallApiAsync(4),
- CallApiAsync(5),
- };
- var whenAllTask = Task.WhenAll(tasks);
- try
- {
- await whenAllTask;
- }
- catch (Exception firstEx)
- {
- Console.WriteLine($"第一个异常: {firstEx.Message}");
- // ✅ 从 Task.Exception 获取所有异常
- if (whenAllTask.Exception != null)
- {
- Console.WriteLine($"\n总共 {whenAllTask.Exception.InnerExceptions.Count} 个任务失败:");
- foreach (var ex in whenAllTask.Exception.InnerExceptions)
- {
- Console.WriteLine($"- {ex.GetType().Name}: {ex.Message}");
- }
- }
- }
- }
复制代码 优点:
- ✅ 封装精良,可复用
- ✅ 同时获取乐成和失败的效果
- ✅ 不丢失任何非常信息
2.5 实战场景:并发调用 API + 容错处理处罚
需求:
- 同时调用 10 个 API
- 答应部门失败,只要有 5 个乐成绩算团体乐成
- 记载全部失败的 API,方便排查
实现:- 第一个异常: Response status code does not indicate success: 404 (Not Found).
- 总共 3 个任务失败:
- - HttpRequestException: Response status code does not indicate success: 404 (Not Found).
- - HttpRequestException: Response status code does not indicate success: 500 (Internal Server Error).
- - TaskCanceledException: The request was canceled due to the configured HttpClient.Timeout of 30 seconds elapsing.
复制代码 使用示例:- public async Task CallMultipleApisAsync()
- {
- var tasks = new[]
- {
- CallApiAsync(1),
- CallApiAsync(2),
- CallApiAsync(3),
- CallApiAsync(4),
- CallApiAsync(5),
- };
- // 先启动所有任务(并发执行)
- var whenAllTask = Task.WhenAll(tasks);
- // 逐个 await,捕获每个任务的异常
- foreach (var task in tasks)
- {
- try
- {
- await task;
- Console.WriteLine("✅ 任务成功");
- }
- catch (Exception ex)
- {
- Console.WriteLine($"❌ 任务失败: {ex.Message}");
- }
- }
- }
复制代码 3️⃣ 配景使命(Fire-and-Forget)的非常处理处罚
3.1 标题:配景使命的非常会被吞掉
场景:启动一个配景使命,不期待它完成。- ✅ 任务成功
- ❌ 任务失败: Response status code does not indicate success: 404 (Not Found).
- ✅ 任务成功
- ❌ 任务失败: Response status code does not indicate success: 500 (Internal Server Error).
- ❌ 任务失败: The request was canceled due to the configured HttpClient.Timeout of 30 seconds elapsing.
复制代码 标题:
- 非常被吞掉,无法排盘标题
- 大概导致未处理处罚的非常(UnobservedTaskException)
- 资源大概没有精确开释
3.2 办理方案 1:使用 TaskScheduler.UnobservedTaskException
- public static async Task<(List<T> Successes, List<Exception> Failures)> SafeWhenAll<T>(
- this IEnumerable<Task<T>> tasks)
- {
- var taskList = tasks.ToList();
- var successes = new List<T>();
- var failures = new List<Exception>();
- foreach (var task in taskList)
- {
- try
- {
- var result = await task;
- successes.Add(result);
- }
- catch (Exception ex)
- {
- failures.Add(ex);
- }
- }
- return (successes, failures);
- }
复制代码 缺点:
- 只在垃圾接纳时触发,大概延长很久
- .NET Core 默认不会导致步伐瓦解,轻易忽略非常
3.2 办理方案 2:SafeFireAndForget 扩展方法(保举)
- public async Task CallMultipleApisAsync()
- {
- var tasks = new[]
- {
- CallApiAsync(1),
- CallApiAsync(2),
- CallApiAsync(3),
- CallApiAsync(4),
- CallApiAsync(5),
- };
- var (successes, failures) = await tasks.SafeWhenAll();
- Console.WriteLine($"✅ 成功: {successes.Count} 个");
- Console.WriteLine($"❌ 失败: {failures.Count} 个");
- if (failures.Any())
- {
- Console.WriteLine("\n失败详情:");
- foreach (var ex in failures)
- {
- Console.WriteLine($"- {ex.GetType().Name}: {ex.Message}");
- }
- }
- }
复制代码 使用示例:- ✅ 成功: 2 个
- ❌ 失败: 3 个
- 失败详情:
- - HttpRequestException: Response status code does not indicate success: 404 (Not Found).
- - HttpRequestException: Response status code does not indicate success: 500 (Internal Server Error).
- - TaskCanceledException: The request was canceled due to the configured HttpClient.Timeout of 30 seconds elapsing.
复制代码 优点:
- ✅ 非常不会被吞掉
- ✅ 可以自界说非常处理处罚逻辑
- ✅ 代码清楚,意图明白
3.3 办理方案 3:使用 BackgroundService(.NET Core)
假如是长期运行的配景使命,保举使用 IHostedService 或 BackgroundService。- public class ApiAggregator
- {
- private readonly HttpClient _httpClient;
- private readonly ILogger _logger;
- public ApiAggregator(HttpClient httpClient, ILogger logger)
- {
- _httpClient = httpClient;
- _logger = logger;
- }
- public async Task GetAggregatedDataAsync(
- IEnumerable<string> apiUrls,
- int minSuccessCount = 5,
- CancellationToken cancellationToken = default)
- {
- var tasks = apiUrls.Select(url => CallApiWithLoggingAsync(url, cancellationToken)).ToList();
- var (successes, failures) = await tasks.SafeWhenAll();
- // 记录失败信息
- foreach (var ex in failures)
- {
- _logger.LogError(ex, "API 调用失败");
- }
- // 检查是否满足最低成功数量
- if (successes.Count < minSuccessCount)
- {
- throw new InvalidOperationException(
- $"API 调用失败过多:期望至少 {minSuccessCount} 个成功,实际只有 {successes.Count} 个成功");
- }
- return new AggregatedResult
- {
- Successes = successes,
- FailureCount = failures.Count,
- Errors = failures.Select(ex => ex.Message).ToList()
- };
- }
- private async Task CallApiWithLoggingAsync(string url, CancellationToken cancellationToken)
- {
- try
- {
- _logger.LogInformation("开始调用 API: {Url}", url);
- var response = await _httpClient.GetStringAsync(url, cancellationToken);
- var data = JsonSerializer.Deserialize(response);
- _logger.LogInformation("API 调用成功: {Url}", url);
- return data;
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "API 调用失败: {Url}", url);
- throw; // 重新抛出,由 SafeWhenAll 捕获
- }
- }
- }
- public class AggregatedResult
- {
- public List Successes { get; set; }
- public int FailureCount { get; set; }
- public List<string> Errors { get; set; }
- }
复制代码 4️⃣ AggregateException 的高级用法
4.1 Flatten():扁平化嵌套非常
标题:嵌套的 Task.WhenAll 会产生嵌套的 AggregateException。- var apiUrls = new[]
- {
- "https://api1.example.com/data",
- "https://api2.example.com/data",
- "https://api3.example.com/data",
- // ... 更多 API
- };
- try
- {
- var result = await aggregator.GetAggregatedDataAsync(apiUrls, minSuccessCount: 5);
- Console.WriteLine($"✅ 成功获取 {result.Successes.Count} 个数据");
- Console.WriteLine($"❌ 失败 {result.FailureCount} 个请求");
- }
- catch (InvalidOperationException ex)
- {
- Console.WriteLine($"❌ {ex.Message}");
- }
复制代码 输出:- // ❌ 错误示例:异常会被吞掉
- public void StartBackgroundTask()
- {
- _ = DoWorkAsync(); // Fire-and-forget
- }
- private async Task DoWorkAsync()
- {
- await Task.Delay(1000);
- throw new InvalidOperationException("后台任务失败了!");
- // ❌ 这个异常不会被捕获,程序不会崩溃,但异常信息丢失
- }
复制代码 4.2 Handle():选择性处理处罚非常
场景:某些非常可以忽略,某些非常必要重新抛出。- // 在程序启动时注册全局异常处理器
- TaskScheduler.UnobservedTaskException += (sender, e) =>
- {
- Console.WriteLine($"❌ 未观察到的异常: {e.Exception.Message}");
- // 标记为已观察,防止程序崩溃
- e.SetObserved();
- };
复制代码 活动:
- Handle() 返回 true:非常被处理处罚,不会重新抛出
- Handle() 返回 false:非常未处理处罚,会重新抛出
5️⃣ 非常处理处罚的最佳实践
5.1 优先使用 await + try-catch
- public static async void SafeFireAndForget(
- this Task task,
- Action<Exception> onException = null)
- {
- try
- {
- await task;
- }
- catch (Exception ex)
- {
- // 调用自定义异常处理器
- onException?.Invoke(ex);
- // 或者记录日志
 - Console.WriteLine($"❌ 后台任务异常: {ex.Message}");
- }
- }
复制代码- public void StartBackgroundTask()
- {
- DoWorkAsync().SafeFireAndForget(ex =>
- {
- _logger.LogError(ex, "后台任务失败");
- // 可以发送告警、记录到数据库等
- });
- }
- private async Task DoWorkAsync()
- {
- await Task.Delay(1000);
- throw new InvalidOperationException("后台任务失败了!");
- }
复制代码 5.2 Task.WhenAll 必要全部非常时
- public class MyBackgroundService : BackgroundService
- {
- private readonly ILogger<MyBackgroundService> _logger;
- public MyBackgroundService(ILogger<MyBackgroundService> logger)
- {
- _logger = logger;
- }
- protected override async Task ExecuteAsync(CancellationToken stoppingToken)
- {
- while (!stoppingToken.IsCancellationRequested)
- {
- try
- {
- await DoWorkAsync(stoppingToken);
- }
- catch (Exception ex)
- {
- // ✅ 异常会被记录,服务继续运行
- _logger.LogError(ex, "后台任务执行失败");
- // 等待一段时间后重试
- await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken);
- }
- }
- }
- private async Task DoWorkAsync(CancellationToken cancellationToken)
- {
- // 业务逻辑
- await Task.Delay(5000, cancellationToken);
- }
- }
复制代码 5.3 配景使命必须有非常处理处罚
- var task1 = Task.Run(() => throw new InvalidOperationException("Task 1"));
- var task2 = Task.Run(() => throw new ArgumentException("Task 2"));
- var outerTask = Task.Run(() =>
- {
- Task.WaitAll(task1, task2); // 内层 AggregateException
- });
- try
- {
- outerTask.Wait(); // 外层 AggregateException
- }
- catch (AggregateException aggEx)
- {
- // aggEx.InnerExceptions[0] 是另一个 AggregateException
- // 需要递归处理
- // ✅ 使用 Flatten() 扁平化
- var flattenedEx = aggEx.Flatten();
- foreach (var ex in flattenedEx.InnerExceptions)
- {
- Console.WriteLine($"- {ex.GetType().Name}: {ex.Message}");
- }
- }
复制代码 5.4 库代码不要吞掉非常
- - InvalidOperationException: Task 1
- - ArgumentException: Task 2
复制代码 5.5 非常信息要富足具体
- try
- {
- Task.WaitAll(tasks);
- }
- catch (AggregateException aggEx)
- {
- aggEx.Handle(ex =>
- {
- // 如果是 TaskCanceledException,忽略它
- if (ex is TaskCanceledException)
- {
- Console.WriteLine("任务被取消,忽略");
- return true; // 标记为已处理
- }
- // 其他异常不处理,会重新抛出
- return false;
- });
- }
复制代码 6️⃣ 实战总结:非常处理处罚清单
✅ Do's(应该做的)
场景保举做法单个使命使用 await + try-catch多个使命(必要全部非常)await Task.WhenAll + 查抄 Task.Exception多个使命(容错)使用 SafeWhenAll 扩展方法配景使命使用 SafeFireAndForget 或 BackgroundService嵌套非常使用 Flatten() 扁平化选择性处理处罚使用 Handle() 方法记载日记在 catch 块中使用 ILogger❌ Don'ts(不应该做的)
场景标题使用 .Wait() 或 .Result壅闭线程,大概死锁吞掉非常(空 catch)匿伏标题,难以排查忽略配景使命非常资源走漏,标题难以发现只捕捉第一个非常丢失其他失败信息非常信息不敷难以定位标题7️⃣ 本章小结
核心知识点
- AggregateException 的筹划头脑:
- 为并发使命筹划的非常容器
- 可以包罗多个子非常
- 提供 Flatten() 和 Handle() 高级功能
- await vs Wait/Result 的非常活动:
- await:抛出第一个原始非常,简化处理处罚
- .Wait() / .Result:抛出 AggregateException,壅闭线程
- 优先使用 await
- Task.WhenAll 的非常处理处罚:
- await 只抛出第一个非常
- 通过 Task.Exception 获取全部非常
- 使用 SafeWhenAll 封装处理处罚逻辑
- 配景使命的非常处理处罚:
- 非常轻易被吞掉
- 使用 SafeFireAndForget 或 BackgroundService
- 注册全局的 UnobservedTaskException 处理处罚器
进阶思索
之前去口试,遇见了一道口试题,至今影象犹新。如今拿出来,供各人思索一下:
<blockquote>
有一组API,数量记为N,50 |