AppDomain未处理异常在异步ForEach中失效问题咨询
看起来你的问题出在两个关键地方:你的ForEachAsync扩展方法实现有漏洞,导致异步任务的异常没被正确捕获;另外你可能没处理.NET中专门针对未观察Task异常的事件。
1. 你的ForEachAsync实现大概率没正确等待异步的body任务
从你给出的代码片段来看,很可能你在处理每个分区内的item时,只是调用了body(item)但没有await它——这会导致body抛出的异步异常成为未观察到的Task异常,这些异常不会被Task.WhenAll捕获,反而会在后台线程中“逃逸”,触发调试器的未处理异常提示,甚至在不同.NET版本中有不同的行为(比如.NET Framework中会触发AppDomain崩溃,.NET Core+中调试器会提示但运行时会吞噬)。
另外,你可能也没正确释放IPartition实例(它是IDisposable),这可能带来额外的资源泄漏或Dispose时的异常问题。
修正后的ForEachAsync实现
public static class Extensions { public static async Task ForEachAsync<T>(this IEnumerable<T> source, int degreeOfParallelism, Func<T, Task> body) { // 先做参数校验,避免空引用或无效参数 if (source == null) throw new ArgumentNullException(nameof(source)); if (degreeOfParallelism <= 0) throw new ArgumentOutOfRangeException(nameof(degreeOfParallelism)); if (body == null) throw new ArgumentNullException(nameof(body)); // 获取分区 var partitions = Partitioner.Create(source).GetPartitions(degreeOfParallelism); using var partitionEnumerator = partitions.GetEnumerator(); var processingTasks = new List<Task>(); while (partitionEnumerator.MoveNext()) { var partition = partitionEnumerator.Current; // 每个分区单独启动一个任务处理 processingTasks.Add(ProcessPartitionAsync(partition, body)); } // 等待所有分区处理完成 await Task.WhenAll(processingTasks).ConfigureAwait(false); } private static async Task ProcessPartitionAsync<T>(IPartition<T> partition, Func<T, Task> body) { // using自动释放分区资源 using (partition) { // 迭代分区内的每个item while (partition.MoveNext()) { var item = partition.Current; // 一定要await body的异步任务,确保异常被捕获并聚合到Task中 await body(item).ConfigureAwait(false); } } } }
这个修正后的版本做了这些关键改进:
- 用
await等待body的异步任务,确保所有异步异常被包装到任务中 - 用
using语句自动释放IPartition,避免资源泄漏和Dispose异常 - 增加参数校验,提前暴露无效输入问题
2. 你可能只注册了AppDomain.UnhandledException,但没处理TaskScheduler.UnobservedTaskException
在.NET中,未观察到的Task异常(比如你之前没awaitbody导致的情况)会触发TaskScheduler.UnobservedTaskException事件,而不是直接触发AppDomain.UnhandledException(.NET Framework中最终会触发,但.NET Core+中默认不会)。
所以你需要在Program中同时注册这个事件来捕获这类异常:
// 处理未观察到的Task异常 TaskScheduler.UnobservedTaskException += (sender, args) => { // 记录你的异常日志 LogError("未观察到的Task异常", args.Exception); // 标记异常已处理,避免后续触发AppDomain.UnhandledException(可选,根据你的需求) args.SetObserved(); }; // 原来的AppDomain崩溃日志 AppDomain.CurrentDomain.UnhandledException += (sender, args) => { var exception = args.ExceptionObject as Exception; if (exception != null) { LogError("AppDomain未处理异常", exception); } };
为什么之前的日志器在外部场景正常?
因为外部场景的异常要么是同步抛出的,要么是被正确await的Task异常——这些异常会沿着调用栈向上传递,最终触发AppDomain.UnhandledException。而ForEachAsync里的异常因为没被正确await,成为了“游离”的异步异常,需要专门的事件来捕获。
内容的提问来源于stack exchange,提问作者Ma Dude

