为什么 SemaphoreSlim
是 C# 异步并发控制的首选
因为它是 .NET 原生支持异步等待的信号量实现,
WaitAsync()方法不会阻塞线程,而传统
Semaphore的
WaitOne()是同步阻塞的,在 async/await 场景下会浪费线程资源甚至引发死锁。
常见错误是误用
Semaphore配合
Task.Run(() => sem.WaitOne())—— 这只是把同步等待扔进线程池,并未真正异步化,还增加了调度开销。
SemaphoreSlim构造时传入的
initialCount表示初始可用许可数,比如
new SemaphoreSlim(3)允许最多 3 个操作同时执行 必须配对调用
WaitAsync()和
Release(),否则许可会泄漏,最终所有后续调用都会挂起 建议始终用
try/finally或
using(需封装为可释放的 wrapper)确保
Release()执行,尤其在有异常可能的业务逻辑中
如何安全地用 SemaphoreSlim
包裹异步操作
最直接的方式是在进入临界异步逻辑前
await semaphore.WaitAsync(),执行完后
semaphore.Release()。注意:不能用
await semaphore.Release()—— 它不是异步方法,也没有返回
Task。
典型易错点:在
catch块里忘了
Release(),或在
return前遗漏释放,导致许可永久丢失。
private static readonly SemaphoreSlim _sem = new SemaphoreSlim(2);
<p>public async Task<string> FetchWithLimitAsync(string url)
{
await _sem.WaitAsync();
try
{
using var client = new HttpClient();
return await client.GetStringAsync(url);
}
finally
{
_sem.Release(); // 必须放在这里
}
}
WaitAsync()
的超时和取消怎么设才合理
生产环境几乎都应该设置超时或取消令牌,否则一个卡住的依赖(如慢 API、网络中断)会让整个信号量被占满,后续请求无限排队。
传入TimeSpan:例如
await _sem.WaitAsync(TimeSpan.FromSeconds(5)),超时抛出
OperationCanceledException传入
CancellationToken:适合与外部取消联动,比如 ASP.NET Core 中绑定
HttpContext.RequestAborted两个参数可以同时用:
await _sem.WaitAsync(TimeSpan.FromSeconds(3), token),任一条件满足即退出等待 注意:超时后信号量本身状态不变,无需也**不能**调用
Release()—— 因为你根本没拿到许可
多个 SemaphoreSlim
实例的生命周期和共享范围
并发限制通常按逻辑维度隔离:全局限流用
static实例;按租户/用户限流需用字典缓存,键为租户 ID;而按 HTTP 客户端实例限流,则应作为成员变量注入。
容易被忽略的是内存泄漏风险:如果用
ConcurrentDictionary<string semaphoreslim></string>动态创建但不清理长期不用的 key,
SemaphoreSlim实例会一直驻留。 避免在每次请求都 new 一个
SemaphoreSlim,它不是轻量对象,内部有同步原语和等待队列开销 若需动态限流策略(如根据 QPS 调整并发数),可封装一层,提供
UpdateMaxCount(int newCount)方法,内部调用
Release()或
WaitAsync()调整当前占用差额
SemaphoreSlim不是线程安全的“计数器”,它的
CurrentCount属性只供观察,不能用于条件判断(竞态)
实际使用中最麻烦的从来不是写那几行
WaitAsync和
Release,而是想清楚“这个限制到底要作用在什么粒度上”以及“许可漏了有没有兜底手段”。
