.NET 4.6.2中SemaphoreSlim限制并行任务时部分任务停滞问题
间歇性任务停滞的排查与解决
可能的原因及对应排查方案
1. 信号量释放逻辑存在遗漏
如果任务获取信号量后,下载或上传环节抛出未被捕获的异常,且没有通过try/finally强制释放信号量,会导致信号量计数无法恢复。不过从你描述的“10个任务正常完成、2个卡在等待”来看,这种情况概率不高——要是真有信号量泄漏,后续所有等待的任务都拿不到许可,但你刚好卡2个。不过还是要确认代码里有没有严格包裹释放逻辑:
// 正确写法:不管成功失败都释放信号量 await semaphore.WaitAsync(); try { Log("开始下载"); // 下载、上传API逻辑 } catch (Exception ex) { Log($"执行出错:{ex.Message}"); } finally { semaphore.Release(); Log("信号量已释放"); }
要是没加finally,任务一旦在获取信号量后抛异常,这个信号量许可就永久被占了,后面的任务会一直堵着。
2. ASP.NET请求上下文被回收导致任务挂起
在.NET 4.6.2的传统ASP.NET中,异步任务默认会捕获当前请求的SynchronizationContext。你用的是“即发即弃”模式——请求返回后后台任务还在跑,但ASP.NET会在请求结束后清理这个上下文,导致SemaphoreSlim.WaitAsync()的完成回调没法被调度,任务就永久卡在等待状态。
验证方法:在等待信号量时加上ConfigureAwait(false),跳过请求上下文的捕获:
await semaphore.WaitAsync().ConfigureAwait(false);
这样任务会在线程池线程上执行,不受请求上下文回收的影响。
3. 线程池调度异常或饥饿
虽然你测试的负载很低,但如果之前有高负载残留、线程被阻塞,ASP.NET线程池可能出现调度延迟。不过这种情况一般是短暂的,不会永久停滞。可以通过以下方式排查:
- 查看Windows事件日志里的ASP.NET线程池警告
- 在任务等待前后记录线程ID,确认是否有线程被卡死
4. HttpClient实例被提前销毁
如果你的HttpClient是在请求范围内创建的,请求结束后被Dispose,后台任务里的HttpClient调用会失败。要是异常被吞了(没打日志),就会看起来像任务停滞。建议用单例HttpClient(避免频繁创建销毁),确保后台任务用的实例不会被提前回收。
解决建议
- 必须用
try/finally包裹信号量释放:不管任务执行成功还是报错,都要释放信号量,杜绝泄漏。 - 给
WaitAsync()加ConfigureAwait(false):脱离请求上下文,防止上下文被回收导致任务挂起。 - 尽量避免“即发即弃”模式:传统ASP.NET里后台任务可靠性很差,建议用Hangfire这类专用后台任务框架托管,或者把任务提交到独立线程池队列,别依赖请求上下文。
- 完善异常日志:确保所有异常都被捕获并记录,避免静默异常让任务看起来停滞。
内容的提问来源于stack exchange,提问作者Reddy
相关产品推荐
相关产品推荐

