SemaphoreSlim 不是锁而是限流工具,核心区别在于它是计数型、支持异步、允许多线程并发;误用作互斥锁会导致性能损耗与取消失效,正确场景是控制API调用等并发数。

为什么 SemaphoreSlim 不是锁,但常被误当锁用
它不保证线程执行顺序,也不绑定调用者身份——只管“当前有几个名额空着”。很多人一上来就 Wait() + Release() 套在临界区外,结果发现并发数还是超了。问题出在:没意识到 Wait() 是阻塞点,而释放必须严格配对;一旦异常跳过 Release(),信号量就永久卡死。
正确做法是用 try/finally 包裹业务逻辑,确保 Release() 必然执行:
await _sem.WaitAsync();
try
{
// 访问受限资源
}
finally
{
_sem.Release();
}
-
WaitAsync()支持取消(传CancellationToken),比同步Wait()更安全 - 构造时传的
initialCount是“初始可用数”,不是“最大并发数”——后者由maxCount参数决定 - 别在
using里创建SemaphoreSlim:它不是一次性资源,生命周期应与限流策略一致
SemaphoreSlim 和 lock / Monitor 的关键区别在哪
lock 是线程独占、无计数、不可跨 async;SemaphoreSlim 是计数型、支持异步等待、允许多个线程同时进入——这才是它适合“限流”的根本原因。
典型误用:用 SemaphoreSlim 保护一个变量读写,却设 maxCount = 1。这看似等价于 lock,但代价更高(涉及 Task 调度、状态机),且无法响应取消。真要互斥,就该用 lock 或 Monitor。
- 限流场景选
SemaphoreSlim:比如最多 5 个请求同时调第三方 API - 数据竞争场景选
lock:比如累加一个int字段 -
SemaphoreSlim的CurrentCount是只读快照,不能靠它做条件判断(竞态)
如何避免 WaitAsync 等待超时或永远挂起
默认 WaitAsync() 会一直等,线上服务可能因此堆积大量挂起 Task。必须设超时,否则下游故障会直接拖垮上游。
推荐统一用带超时的重载,并处理 OperationCanceledException 和超时返回 false:
if (!await _sem.WaitAsync(TimeSpan.FromSeconds(3), cancellationToken))
{
throw new InvalidOperationException("获取并发许可超时");
}
- 超时时间不能拍脑袋定:要参考下游 P99 响应时间,再加缓冲(比如 2–3 倍)
- 别把
cancellationToken和超时混用:前者是外部主动取消,后者是内部等待失败,语义不同 -
WaitAsync返回false表示超时未获取到许可,不是异常,别漏判
ASP.NET Core 中全局限流要不要用 SemaphoreSlim
不要。它只作用于单实例内存,K8s 多副本、IIS 多工作进程下完全失效。真正需要的是分布式限流(如 Redis + Lua),或网关层限流(Ocelot、YARP、Envoy)。
如果只是单机内控制后台任务并发(比如定时拉取日志的 WorkerService),SemaphoreSlim 完全合适,且轻量高效。
- Web API 层限流 → 查
Microsoft.AspNetCore.RateLimiting(.NET 7+)或第三方中间件 - 后台服务内部任务节流 →
SemaphoreSlim是首选,注意生命周期注册为Singleton - 别在 Controller Action 里 new 一个
SemaphoreSlim:每次请求都新建,等于没限
最常被忽略的一点:SemaphoreSlim 的 Release() 必须和 WaitAsync() 在同一个实例上调用,且次数严格相等。跨方法、跨类、甚至跨 async 分支乱调,都会导致计数错乱——这种 bug 很难复现,但线上爆发就是雪崩。


















