semaphoreslim 是硬性限制并发数的开关,非加锁工具;必须用双参数构造、复用实例、waitasync配finally释放、手动区分超时与取消,且不保证fifo。

SemaphoreSlim 不是用来加锁的,它是硬性限制并发数的开关——用错位置、漏掉 Release()、初始值设错,都会让限流彻底失效,甚至拖垮整个服务。
WaitAsync() 必须配对 Release(),且只能写在 finally 里
许可泄漏是线上最隐蔽也最致命的问题:一次 WaitAsync() 没对应上一次 Release(),计数就永久少 1;连续几次后,后续所有 WaitAsync() 都会无限挂起,任务队列直接堵死。
错误写法包括:
-
Release()写在try块里 → 异常一抛就跳过,漏释放 -
Release()写在catch里 → 正常执行时又没走,照样漏 - 靠
using包裹SemaphoreSlim→ 它不是资源型对象,Dispose()不影响计数器,纯属无效操作
唯一安全模式是:
await _sem.WaitAsync();
try
{
await DoSomethingAsync();
}
finally
{
_sem.Release();
}
构造时必须显式指定 initialCount 和 maxCount
只传一个参数(如 new SemaphoreSlim(3))等价于 new SemaphoreSlim(3, int.MaxValue),等于没设上限——只要有人误调多次 Release(),许可数就能突破 3,限流形同虚设。
正确做法是始终用双参数构造:
-
initialCount:初始放行数量(比如预热时允许 3 个请求立刻进) -
maxCount:硬性天花板,防止因异常重试、逻辑错误导致重复Release()突破限制
推荐写法:new SemaphoreSlim(3, 3)。这个实例必须复用,不能在方法内 new,否则每个调用都新建一把“没锁的锁”。
超时和取消必须手动区分,别信 WaitAsync(TimeSpan) 的返回值
.NET 6+ 中,WaitAsync(TimeSpan) 已不返回 bool,而是直接抛 OperationCanceledException。你 catch 到它,无法单凭异常类型判断是超时还是主动取消。
可靠做法是用 CancellationTokenSource 显式控制:
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
try
{
await _sem.WaitAsync(cts.Token);
}
catch (OperationCanceledException) when (!cts.Token.IsCancellationRequested)
{
// 真正的超时,不是用户主动取消
throw new TimeoutException("Acquiring semaphore timed out.");
}
// 其他逻辑...
注意:cts.Token.IsCancellationRequested 为 false 才代表超时,这点极易反直觉。
别把它套在 HttpClient.SendAsync() 外层直接包
HttpClient 本身有 MaxConnectionsPerServer 和连接池管理,你再叠一层 SemaphoreSlim 控制“发送瞬间”的并发,只是徒增复杂度,还可能因异常漏 Release() 导致死锁。
真正要控的是「决策发出请求」这一动作,不是「底层网络调用执行中」的状态。合理位置是:
- HttpRequestMessage 构造完成之后
- 调用
SendAsync()之前
也就是说,许可应在请求确定要发、但还没真正触达网络栈时获取,并确保哪怕 SendAsync() 抛出 HttpRequestException,也要在 finally 里归还。
最易被忽略的一点:SemaphoreSlim 不保证 FIFO,高并发下谁先等、谁先进,没有顺序保障。如果业务强依赖执行顺序,得自己加排队队列,它只管“人数”,不管“先后”。











