涉及锁时,所有输入任务已完成但Task.WhenAll仍无法完成的原因排查
涉及锁时,所有输入任务已完成但Task.WhenAll仍无法完成的原因排查
咱们来一步步拆解这个诡异的死锁问题,从代码逻辑到Task.WhenAll的实现细节慢慢捋清楚:
一、先理清楚代码的核心执行流程
先把Test方法里的关键时间线理明白:
- 初始化
batchProcessingTask为已完成任务,创建锁对象batchLock和取消令牌源 - 给取消令牌注册回调:触发取消时,先抢
batchLock,再创建Task.WhenAll([batchProcessingTask])并阻塞等待它完成 - 启动第一轮批处理:
batchProcessingTask被赋值为ProcessBatch()返回的2秒延迟任务T1,同时启动一个Task.Run后台任务 - 等待1秒后触发取消,此时
T1还有1秒才会完成 T1完成后,后台任务会先awaitT1,然后尝试抢batchLock- 这时候取消回调大概率会先抢到
batchLock,进入锁块后执行Task.WhenAll([batchProcessingTask]).Wait()
二、死锁的核心原因:线程池“饥饿”+锁与阻塞的叠加
你调试时看到batchProcessingTask已经完成,但whenAllTask却没完成,看似矛盾,其实问题出在线程池的调度资源被耗尽,加上阻塞操作和锁的叠加效应:
- 回调线程抢到锁后,调用
whenAllTask.Wait(),直接把当前线程池线程给阻塞住了 - 虽然
batchProcessingTask已经完成,但Task.WhenAll的内部完成逻辑(哪怕是同步完成的任务)有时候也需要线程池的调度资源来最终标记自己为完成状态 - 此时线程池因为有线程被阻塞,加上后台任务还在等着抢
batchLock,可用线程被占满,WhenAll任务的完成逻辑根本拿不到执行机会,陷入了“饥饿”状态 - 最关键的是:你是在持有锁的情况下调用的
Wait(),这导致后台任务永远抢不到锁去更新batchProcessingTask,而回调线程因为whenAllTask无法完成,永远不会释放锁——这下就形成了死循环式的死锁
三、为什么直接用batchProcessingTask.Wait()不会死锁?
因为batchProcessingTask已经完成了,Wait()会立即同步返回,不会阻塞线程池线程,回调线程能快速释放锁,后台任务就能顺利抢到锁完成后续逻辑,自然不会出问题。
四、怎么解决这个问题?
给你几个可行的调整方向:
- 缩小锁的范围,别在锁里做阻塞操作:锁只用来保护真正需要同步的逻辑,别把
Wait()这类阻塞操作放在锁里 - 用异步等待代替阻塞:如果必须等任务完成,用
await代替Wait(),但要注意取消回调的异步处理(可以用Task.Run包裹异步逻辑后注册到取消令牌) - 别在闭包里修改被捕获的任务变量:你的
batchProcessingTask被嵌套方法反复重新赋值,这会导致回调里拿到的任务可能不是你预期的那个,大大增加调试难度
举个调整后的回调示例(用异步等待,缩小锁范围):
applicationStoppingCts.Token.Register(async () => { // 先等任务完成,再抢锁 await batchProcessingTask; lock (batchLock) { if(applicationStoppingCts.Token.IsCancellationRequested) return; // 这里只放需要锁保护的核心逻辑 } }, useSynchronizationContext: false);
五、最后总结
这个问题不是Task.WhenAll的bug,而是线程池阻塞导致的调度饥饿,加上锁和阻塞操作的不当叠加才引发的死锁。记住:线程池线程是宝贵的资源,别轻易阻塞它们,尤其是在持有锁的时候。
内容来源于stack exchange
相关产品推荐
相关产品推荐

