SemaphoreSlim用于限制同时执行受控逻辑的并发数,非Task总数;必须配对WaitAsync()与Release(),且Release()须置于try/finally中确保执行,避免许可泄漏。

SemaphoreSlim 不是用来“限制 Task 并发数”的工具,而是用来限制“同时执行某段受控逻辑的并发数”——它不感知 Task 生命周期,只管当前有几个许可可用。
WaitAsync() 必须配对 Release(),否则许可永久泄漏
很多人写完 await _sem.WaitAsync() 就直接跑业务逻辑,忘了释放。一旦中间抛异常、或提前 return,Release() 就永远不会执行,信号量计数卡死,后续所有等待都会挂起。
- 必须用
try/finally包裹,Release()放在finally块里 - 别在
using里创建SemaphoreSlim:它不是一次性资源,Dispose()只清理内部 Task 状态,不影响计数逻辑 - 不要跨不同资源共用同一个实例(比如既控 API 调用又控文件写入),会导致逻辑耦合、调试困难
构造参数 initialCount 和 maxCount 的真实含义
new SemaphoreSlim(2, 5) 表示:初始有 2 个许可可用,但最多允许 5 个线程同时进入 —— 这不是“默认开 2 个,最多扩到 5 个”,而是硬性上限为 5,你只能靠 Release() 把计数从 0 拉回到 ≤5 的范围。
- 如果初始化为
new SemaphoreSlim(0, 3),所有WaitAsync()都会阻塞,直到有人调Release()或Release(3) -
CurrentCount是只读快照,不能用于条件判断(比如if (_sem.CurrentCount > 0) ...),存在竞态风险 - 别把
initialCount当成“预热值”,它只是起点;真正控制并发上限的是maxCount
超时和取消必须显式处理,不能依赖默认行为
WaitAsync() 默认无限等待,线上服务一旦下游响应变慢,就会堆积大量挂起 Task,拖垮整个请求队列。
- 必须传
TimeSpan超时参数,例如await _sem.WaitAsync(TimeSpan.FromSeconds(3), ct) - 返回
false表示超时未获取许可,不是异常,要主动判断,不能只 catchOperationCanceledException -
cancellationToken是外部主动取消(如 HTTP 请求被客户端中止),和超时语义不同,两者需分开处理 - 超时时间建议设为下游 P99 响应时间的 2–3 倍,而非拍脑袋定 1 秒或 10 秒
SemaphoreSlim 不是 lock,别用它做互斥
设 maxCount = 1 看似等价于 lock,但代价高得多:涉及 async 状态机、Task 调度、无法响应取消,且不保证执行顺序。
- 真要保护共享变量读写(如累加
int),用lock或Interlocked -
SemaphoreSlim的适用场景是“限流”:比如最多 5 个并发调第三方 API、最多 3 个并发写磁盘、控制数据库连接池出借速率 - 它不保证 FIFO,高并发下等待线程获取许可的顺序不确定;如需严格顺序,得自己加队列+锁
- 同一线程可多次
WaitAsync()(支持重入),但每次都要对应一次Release(),否则仍泄漏
最常被忽略的一点:SemaphoreSlim 的生命周期必须与限流策略一致——它该是单例、静态字段或 DI 容器管理的 Scoped/Singleton 实例,而不是每次请求 new 一个。


















