semaphoreslim 是限制并发线程数的首选,因其轻量、异步友好、不依赖内核对象且原生支持 waitasync(),可避免线程池阻塞;适用于控制数据库连接、api 调用等资源访问,防止资源耗尽。

为什么 SemaphoreSlim 是限制并发线程数的首选
因为它是轻量、异步友好的信号量实现,专为 .NET 的 async/await 设计。相比老式 Semaphore,它不依赖操作系统内核对象,开销低,且原生支持 WaitAsync() —— 这意味着你不会在线程池里阻塞线程等待许可。
典型使用场景:控制对数据库连接池、外部 API 调用、文件写入或内存敏感操作的并发访问数,防止资源耗尽或服务被压垮。
SemaphoreSlim 初始化和基本等待逻辑
构造时传入最大并发数(如 new SemaphoreSlim(3) 表示最多 3 个线程同时进入),这是硬性上限。
- 调用
Wait()会同步阻塞当前线程,直到获得许可;在 UI 或高吞吐服务中应避免 - 更推荐用
WaitAsync(),它返回Task,可配合await避免线程浪费 - 务必配对调用
Release()(或ReleaseAsync()),否则许可数不会归还,后续等待将永久挂起 - 可传入
TimeSpan参数设定超时,超时后WaitAsync()抛出OperationCanceledException,需捕获处理
示例:
var semaphore = new SemaphoreSlim(2);
try
{
await semaphore.WaitAsync(TimeSpan.FromSeconds(5));
// 执行受保护的操作
}
catch (OperationCanceledException)
{
// 超时未获取到许可
}
finally
{
semaphore.Release(); // 必须执行,哪怕出异常也要确保释放
}
常见错误:忘记释放、跨作用域误用、未处理取消
最常踩的坑不是“怎么写”,而是“写完就跑”导致许可泄漏:
- 在
try块里await操作后抛异常,但finally外没写Release()→ 许可永远丢失 - 把同一个
SemaphoreSlim实例混用于不同资源(比如既控 DB 又控日志)→ 逻辑耦合,难以调试 - 用了
WaitAsync(cancellationToken)却没在catch (OperationCanceledException)后检查cancellationToken.IsCancellationRequested→ 可能误判为超时而非主动取消 - 在
using语句中创建SemaphoreSlim→ 它不是IDisposable的资源型对象,Dispose()仅清理内部Task状态,不释放许可,也不影响并发控制逻辑
进阶注意:公平性、递归调用与性能边界
SemaphoreSlim 默认不保证 FIFO,即等待队列不严格按请求顺序发放许可(尤其在高并发下)。如果你依赖执行顺序,得自己加锁排队,它不解决这个问题。
- 它不阻止同一线程多次
WaitAsync()(即支持“重入”),但每次WaitAsync()都要对应一次Release(),否则仍会泄漏 - 当并发数设为 1,它可替代
lock实现异步临界区,但比lock开销大,纯同步场景优先用lock - 许可数设为 0 是合法的,此时所有
WaitAsync()都会等待,可用于动态暂停流量,但需确保有地方调用Release()唤醒 - 大量线程频繁争抢少量许可时,
SemaphoreSlim内部用自旋 + 等待混合策略,一般够用;若实测 CPU 占用异常高,需检查是否许可数设置过小或等待逻辑存在死锁倾向
真正难的是设计好“谁创建、谁持有、谁释放”的生命周期边界——尤其是跨 async 方法链传递许可状态时,很容易在某个 await 分界点丢掉释放时机。










