You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

ASP.NET6中async Task配合SemaphoreSlim死锁问题咨询

死锁根本原因

你的死锁是同步等待混用异步代码导致线程池耗尽,和SemaphoreSlim本身的实现没有关系,具体触发流程如下:

  • 控制台程序线程池初始工作线程数和逻辑CPU核心数一致(通常为416个),新线程注入速度约为每5001000ms新增1个,速度极慢。
  • 你在DoWork这个异步方法中使用了semaphore.Wait()这个同步阻塞方法等待信号量:任务一旦调用Wait()但没拿到锁,就会一直占着当前线程池线程不释放,直到拿到锁或者超时。
  • 你一次性排队1000个任务到线程池:第一个拿到信号量的任务执行到await Task.Delay(10)时,会将当前线程归还线程池,Delay结束后的续体(包含semaphore.Release()逻辑)会重新投递到线程池队列等待调度。
  • 此时线程池剩余的初始工作线程,全部被其他排队任务占用,这些任务全部卡在semaphore.Wait()的同步阻塞调用上,没有空闲线程。
  • 10ms后Delay到期,释放信号量的续体被投递到线程池队列,但排在它前面的还有近千个等待执行的任务。线程池每新增一个线程,马上就被队列前面卡在Wait()的任务抢走继续阻塞,根本轮不到执行释放锁的续体,最终形成死锁。
对照测试结果的解释

你做的两组对照测试能正常运行,核心原因是没有出现「持有锁的任务需要重新排队等线程池线程才能释放锁」的情况:

  • 将Task.Delay替换为Thread.Sleep时:拿到锁的任务全程不会释放持有的工作线程,Sleep 10ms后直接在当前线程执行semaphore.Release(),不需要依赖额外的空闲线程跑续体,自然不会卡死。
  • 将Task.Run替换为ThreadPool.QueueUserWorkItem时:如果工作项内部没有异步await逻辑,执行流全程同步,和上面Thread.Sleep的场景一致,拿到锁的线程执行完逻辑直接释放锁,不会触发线程池耗尽。
修复方案
  1. 异步场景下永远使用SemaphoreSlim的异步等待方法,替换同步Wait调用:
    把DoWork中的
    if (semaphore.Wait(timeoutSec * 1000))
    
    替换为
    if (await semaphore.WaitAsync(TimeSpan.FromSeconds(timeoutSec)))
    
    异步等待不会阻塞线程池线程,就算有上千个任务在等锁,也不会占用线程,线程池始终有空闲线程处理续体,不会出现耗尽问题。
  2. 异步代码中禁止混用同步阻塞调用:不要在async方法中使用.Wait()、.Result、同步版本的Wait等阻塞API,这是异步场景下死锁和线程池耗尽的最高频诱因。
  3. (非死锁相关bug修复)你代码中循环启动的Task没有加入tasks列表,Task.WaitAll实际等待的是空任务列表;另外successCount/failedCount/doneCounter多线程修改没有加锁,存在竞态条件,会出现计数不准的问题。
生产环境ASP.NET 6应用偶发冻结的原因

和测试代码的根因完全一致:平时流量平稳时线程池线程充足,不会触发耗尽;当遇到流量波动、请求量突增时,大量请求同时卡在SemaphoreSlim的同步Wait调用上占满线程池,持有锁的请求的异步续体没有线程可调度,无法释放锁,最终整个应用无响应。这种问题因为需要线程池积压到阈值才会触发,所以往往会在运行数周、遇到特定流量场景时才出现。


内容的提问来源于stack exchange,提问作者michlG

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.29 12:54:25