semaphoreslim 必须配对使用 waitasync() 和 release(),且 release() 唯一安全位置是 finally 块;必须复用实例而非每次 new;需显式设置超时或 cancellationtoken;不可替代 lock,适用场景为硬性并发数限制。

WaitAsync() 必须配对 Release(),且只能写在 finally 里
许可泄漏是 SemaphoreSlim 最隐蔽也最致命的问题:一次 WaitAsync() 没对应上一次 Release(),计数就少一个;几次之后,所有后续调用都会永久挂起,服务看似“卡住”实则死锁。
常见错误包括:
- 把
Release()写在try块里——异常一抛,直接跳过 - 在
catch里再写一遍Release()——重复释放导致许可溢出(比如从 0 变成 1),限流完全失效 - 方法中途
return,忘了释放
正确结构只有一种:
await _sem.WaitAsync();
try
{
await DoSomethingAsync();
}
finally
{
_sem.Release(); // 唯一安全位置
}
别在方法里 new SemaphoreSlim,必须复用静态或注入实例
每次调用都 new SemaphoreSlim(5),等于每回都新开一个“空桶”,根本起不到限流作用——并发数还是爆满。
它不是一次性工具,而是长期存活的协调器。生命周期必须匹配限流策略范围:
- 全局限流 →
private static readonly SemaphoreSlim字段 - 按租户/用户限流 → 注入为 scoped service,构造时传入 key 或配置
- 绝对不要用
using包裹 ——Dispose()不影响计数,也不能防泄漏
初始化参数建议设成相同值,例如 new SemaphoreSlim(5, 5),避免 initialCount 引发的动态补发逻辑混乱。
超时和取消必须显式传参,不能只靠 try/catch
WaitAsync() 默认无限等待。线上一个下游卡住,就会把整个信号量池堵死,后续请求全排队挂起。
必须主动控制等待边界:
- 用
TimeSpan控制最大等待时长:await _sem.WaitAsync(TimeSpan.FromSeconds(3)),超时抛OperationCanceledException,但cancellationToken.IsCancellationRequested == false,需捕获后判断是否真超时 - 用
CancellationToken响应外部取消(如 ASP.NET Core 的HttpContext.RequestAborted) - 两个参数可共存:
await _sem.WaitAsync(TimeSpan.FromSeconds(3), token),任一条件满足即退出 - 注意:超时或取消后,你根本没拿到许可,绝不能调用
Release()
SemaphoreSlim 不是锁,别拿它保护单个变量读写
有人用 new SemaphoreSlim(1) 替代 lock,这是高成本低收益的误用。它不保证执行顺序、不绑定线程身份、调度开销比 Monitor 高得多,还无法响应取消。
真正该用它的场景,是“硬性限制并发数”:
- 最多同时发 5 个 HTTP 请求(控 API 调用频次)
- 最多 3 个并发文件写入(防磁盘 I/O 打满)
- 后台任务吞吐不超过 10 个/秒(保内存稳定)
而累加计数器、修改共享字段这类操作,该用 Interlocked 或 lock —— 简单、快、语义清晰。
最后提醒一句:CurrentCount 是只读快照,竞态下不可信,别拿它做 if 判断依据。











