C# 异步信号量Async Semaphore C#如何限制异步操作的并发度

来源:这里教程网 时间:2026-02-21 17:42:41 作者:

为什么
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
,而是想清楚“这个限制到底要作用在什么粒度上”以及“许可漏了有没有兜底手段”。

相关推荐