SemaphoreSlim 是 C# 限流可靠选择但易出错:需 try/finally 配对 WaitAsync/Release;initialCount 和 maxCount 必须显式指定防突破限制;超时抛 OperationCanceledException 而非 TimeoutException;不保证 FIFO,保序需额外队列;不跨进程、无自动释放。

SemaphoreSlim 是 C# 中限制并发的可靠选择,但直接 new 一个就用,大概率会在线上出问题——它不自动释放、不保序、不跨进程,且超时和取消逻辑容易写错。
WaitAsync() 必须配对 Release(),且必须用 try/finally
常见错误是只调用 WaitAsync(),忘了 Release();或在异常路径下漏掉释放,导致许可永久泄漏,后续所有任务卡死等待。
-
WaitAsync()获取许可后,必须确保无论成功、异常、取消,都执行一次Release() - 不能靠 using 或 Dispose() 自动清理:
SemaphoreSlim不是资源型对象,Dispose()只清理内部 Task 状态,不影响计数器 - 正确写法只有这一种模式:
await semaphore.WaitAsync(); try { // 执行受控操作 } finally { semaphore.Release(); }
构造时 initialCount 和 maxCount 的区别直接影响限流效果
很多人传 new SemaphoreSlim(3) 就以为最多 3 个并发,其实这只是初始可用数;若中途有人误调 Release() 超过上限,计数器会突破限制——除非你显式设 maxCount 并配合校验。
-
initialCount:创建时“开门放行”的数量,比如预热连接池可设为 3 -
maxCount:硬性上限,防止Release()被多调(如异常重试中重复释放),应始终显式指定:new SemaphoreSlim(3, 3) - 若只传一个参数(如
new SemaphoreSlim(3)),等价于new SemaphoreSlim(3, int.MaxValue),失去上限保护
WaitAsync(CancellationToken) 超时后抛的是 OperationCanceledException,不是 TimeoutException
这是最容易踩的异常处理坑。用 TimeSpan 重载(如 WaitAsync(TimeSpan.FromSeconds(5)))会抛 OperationCanceledException,不是直观的超时异常类型,catch 时容易遗漏。
- 推荐统一用
CancellationToken方式,并手动构造带超时的 token:using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); await semaphore.WaitAsync(cts.Token);
- 捕获时必须 catch
OperationCanceledException,且要检查cts.Token.IsCancellationRequested来区分是主动取消还是超时 - 不要依赖
WaitAsync(TimeSpan)的返回 bool 值——它已废弃,.NET 6+ 中该重载仅抛异常,不返回 false
SemaphoreSlim 不保证 FIFO,高并发下请求顺序和执行顺序可能错乱
如果你的业务依赖“先到先服务”(比如排队扣库存、日志写入保序),SemaphoreSlim 本身不提供队列保序能力,底层异步等待队列在竞争激烈时会打乱顺序。
- 它内部使用
TaskCompletionSource队列,但调度由 .NET 任务调度器决定,不严格按等待时间排序 - 需要保序?得自己加一层线程安全的队列(如
ConcurrentQueue)+ManualResetValueTaskSource或简单用lock+Monitor.Wait/Pulse - 别指望
Release()后下一个WaitAsync()一定拿到许可——可能被其他新来的任务抢走
真正难的不是写对那几行代码,而是想清楚:你的场景是否真的只需要“数量限制”,还是隐含了顺序、公平性、跨进程或可观测性需求——这些 SemaphoreSlim 都不负责,得你自己补全。











