CancellationToken.ThrowIfCancellationRequested抛异常,求排查是自身问题还是.NET Bug?
咱们先从代码逻辑入手排查,毕竟这类取消相关的异常,绝大多数都是代码细节没处理到位,而非.NET的罕见Bug(当然也不能完全排除,但先抓常见原因)。
先还原你的核心代码场景
根据你的描述,你的代码结构大概是这样的:
var semaphore = new SemaphoreSlim(3); var cancelSource = new CancellationTokenSource(); var doNothingTasks = new List<Task>(); for (int i = 0; i < 10; i++) { doNothingTasks.Add(Task.Factory.StartNew(async () => { await semaphore.WaitAsync(cancelSource.Token); try { // 你的DoNothing方法逻辑 await doNng.DoNothing(cancelSource.Token); } finally { semaphore.Release(); } }, cancelSource.Token)); } Thread.Sleep(3000); cancelSource.Cancel(); // 期望用ContinueWith处理取消后的逻辑 foreach (var task in doNothingTasks) { task.ContinueWith(t => { if (t.IsCanceled) { Console.WriteLine("任务已取消"); } else if (t.IsFaulted) { Console.WriteLine($"任务出错:{t.Exception?.InnerException?.Message}"); } }, TaskContinuationOptions.ExecuteSynchronously); } await Task.WhenAll(doNothingTasks);
最可能触发意外异常的代码问题
1. Task.Factory.StartNew和异步lambda的坑
你用Task.Factory.StartNew启动异步lambda时,它返回的是Task<Task>而非普通的Task——也就是说,你加到doNothingTasks里的是外层的启动任务,真正执行DoNothing的是内层的异步任务。当你调用cancel.Cancel()时,外层任务可能早就完成了(它只是启动了异步操作),但内层任务的取消异常没被正确捕获,就会意外冒泡出来。
修复方案:把Task.Factory.StartNew换成Task.Run,它会自动解包异步lambda的返回值,拿到真正代表DoNothing执行过程的Task。
2. SemaphoreSlim.WaitAsync的取消没处理好
如果在semaphore.WaitAsync(cancelToken)等待过程中触发取消,这个方法本身会抛出OperationCanceledException。如果你的代码没把这个调用放到try块里,异常就会直接跑出来,导致你看到的“意外异常”。
修复方案:把WaitAsync的调用也包含在try块内,或者在ContinueWith里同时处理IsCanceled和IsFaulted的情况。
3. ContinueWith加晚了
如果你是在调用cancel.Cancel()之后才给任务加ContinueWith,可能有些任务已经进入了Faulted或Canceled状态,这时ContinueWith会立即执行,但如果任务的异常没被正确捕获,还是会漏出来。
修复方案:在创建任务的时候就附加ContinueWith,或者确保在await Task.WhenAll之前就处理好所有任务的状态。
4. DoNothing内部的取消逻辑有问题
如果DoNothing方法里调用ThrowIfCancellationRequested()的时机不对——比如在某个非线程安全的操作中间抛出,或者取消后没正确释放资源——也可能导致意外异常。
检查点:确保DoNothing里的ThrowIfCancellationRequested()是在安全的时机调用,所有资源都通过finally或者using语句释放。
什么时候才考虑是.NET Bug?
如果上面的代码问题都排查完了,还是出现意外异常,那才需要怀疑是不是.NET的罕见Bug:
- 抛出的不是
OperationCanceledException,而是其他完全没预期的异常(比如NullReferenceException、ObjectDisposedException) - 复现场景非常稳定,而且在不同版本的.NET里都能复现(比如同时在.NET 6和.NET 8中出现)
- 你已经完整捕获了所有可能的取消异常,但还是有异常从任务里冒出来
这种情况下,你要收集完整的异常栈信息(包括异常类型、消息、调用栈),然后去.NET的官方仓库提交Issue。
总结排查步骤
- 先把
Task.Factory.StartNew换成Task.Run,解决异步任务解包的问题 - 确保所有可能触发取消的操作(
WaitAsync、ThrowIfCancellationRequested)都被try/catch包裹,或者在ContinueWith里处理好任务的IsCanceled和IsFaulted状态 - 检查
DoNothing方法内部的取消逻辑是否正确 - 如果以上都没问题,再考虑是不是.NET Bug,收集详细异常信息进一步排查
内容的提问来源于stack exchange,提问作者AviFarah

