semaphoreslim 用于硬性限制并发数而非加锁,必须复用实例、初始许可数大于0,并在 finally 中配对 release() 防泄漏;超时与取消需显式处理,禁用 using 和方法内新建。

SemaphoreSlim 不是用来“加锁”的,而是用来做**并发数硬性限制**的。它适合控制同时执行的异步操作数量,比如防止瞬间打爆下游 API、避免连接池耗尽、或约束后台任务吞吐。用错地方或漏掉关键步骤,轻则限流失效,重则整个服务卡死。
WaitAsync() 必须配对 Release(),且只能在 finally 里写
最常见的崩溃点不是语法错误,而是许可泄漏:一次 WaitAsync() 没对应上一次 Release(),计数就少一个;累积几次,后续所有 WaitAsync() 都会永久挂起。
必须写成这样:
await _sem.WaitAsync();
try
{
await DoSomethingAsync();
}
finally
{
_sem.Release();
}
-
Release()不能放在try块里——异常一抛,就跳过了 -
catch块里再写一遍Release()是重复释放,会导致许可数溢出(比如从 0 变成 1),后续限流完全失效 - 不要用
using包裹SemaphoreSlim——它不是资源型对象,Dispose()不影响许可计数,也解决不了泄漏问题
初始化时别传 0,也别在方法里 new
SemaphoreSlim 实例必须复用,且初始许可数不能为 0。
- 写成
new SemaphoreSlim(0, 5):所有WaitAsync()立即挂起,没机会释放,等于锁死 - 写成
new SemaphoreSlim(5, 5)是安全的——初始就有 5 个许可,最大也不超 5 - 如果声明在方法内:
void Handle() { var sem = new SemaphoreSlim(3); ... },每次调用都新建,完全不起限流作用 - 正确做法是作为
private static readonly字段,或注入到服务生命周期中(如 ASP.NET Core 的 scoped service)
别把它套在 HttpClient.SendAsync 上层直接包
你真正要控的是「发起请求的并发数」,不是「SendAsync 执行中的线程」。HttpClient 自带连接池(MaxConnectionsPerServer),底层已经做了并发管理。
- 错误做法:
await _sem.WaitAsync(); var res = await _client.SendAsync(req).ConfigureAwait(false); _sem.Release();—— 这只是叠了一层无意义的锁,还可能因异常漏Release()导致死锁 - 正确位置:在构造完
HttpRequestMessage、准备调用SendAsync之前,也就是「请求已确定要发,但还没真正发出」那一刻 - 更合理的结构是封装一层:先
await _sem.WaitAsync(),再进try/finally发请求,哪怕SendAsync抛HttpRequestException,也要确保Release()
超时和取消必须显式处理,不能只靠 try/catch
WaitAsync() 支持两个可选参数:TimeSpan 和 CancellationToken,但它们触发的异常类型不同,处理方式也不同。
-
await _sem.WaitAsync(TimeSpan.FromSeconds(3))超时 → 抛OperationCanceledException,但cancellationToken.IsCancellationRequested == false,得靠捕获后判断是否真超时 -
await _sem.WaitAsync(token)被主动取消 → 同样抛OperationCanceledException,但此时token.IsCancellationRequested == true - 如果两个都传:
await _sem.WaitAsync(TimeSpan.FromSeconds(3), token),任一条件满足即退出,异常来源需区分处理 - 不传任何参数的
WaitAsync()会无限等待,生产环境禁止使用
SemaphoreSlim 的核心复杂点从来不在“怎么写”,而在于“谁负责释放”和“释放时机是否绝对可靠”。哪怕逻辑再简单,只要漏一次 Release(),整个信号量就不可逆地退化为单线程队列——这种故障不会报错,只会让系统越来越慢,直到某次发布后突然全链路延迟飙升。











