Semaphore不能用于异步限流,因无WaitAsync()方法,仅支持同步阻塞的WaitOne();需异步时应使用SemaphoreSlim;仅跨进程同步等极少数场景才必须用Semaphore。

Semaphore 不能用于异步限流,跨进程同步也仅在极少数场景下必须用它——多数情况下你该换用 SemaphoreSlim。
为什么 Semaphore.WaitAsync() 编译不过?
因为 Semaphore 根本没有 WaitAsync() 方法。它是 .NET Framework 1.1 就存在的内核级同步原语,所有等待都是同步阻塞的:WaitOne() 会真实占用线程池线程。你看到的“WaitAsync”调用,实际是误用了 SemaphoreSlim 的 API,或者 IDE 自动补全误导了你。
常见错误现象:
- 写
await semaphore.WaitAsync()→ 编译报错:'Semaphore' does not contain a definition for 'WaitAsync' - 强行用
Task.Run(() => semaphore.WaitOne())包一层 → 线程池被吃光,ASP.NET Core 下请求堆积、超时雪崩
正确做法只有一条:需要异步等待,就别碰 Semaphore,直接用 SemaphoreSlim。
什么时候非得用 Semaphore?
只有当你需要两个或多个独立进程(不是线程)协同访问同一系统资源时,才必须用 Semaphore。它靠 Windows 内核对象 + 名称暴露实现跨进程可见性。
典型使用场景:
- 一个 Windows Service 和一个 WPF 桌面程序共同控制同一台串口设备
- Web API 进程和后台日志归档进程竞争写入同一个全局日志文件(注意:文件锁本身不跨进程,需额外协调)
- 多个 .NET 进程共享一个硬件加密模块,必须串行调用其 SDK
关键实操点:
- 构造时必须传名称:
new Semaphore(1, 1, "MyHardwareLock") - 另一进程用
Semaphore.OpenExisting("MyHardwareLock")获取句柄,不能 new 新实例 - 调用
WaitOne()前建议加超时,避免死锁:semaphore.WaitOne(TimeSpan.FromSeconds(5)) - 释放必须配对,且不能由不同进程乱 Release —— 它不检查调用者身份,谁 Release 谁算数
Semaphore 和 SemaphoreSlim 性能差在哪?
差距不在“快多少”,而在“等得有多重”。Semaphore 每次 WaitOne() 都触发用户态→内核态切换,哪怕只等 1ms,也要付出上下文切换成本;而 SemaphoreSlim 默认先自旋几十纳秒,抢到就走,没抢到才退化为内核事件。
实测影响:
- 高并发短临界区(如 HTTP 客户端并发控流):
SemaphoreSlim耗时约 8ms / 1000 次,Semaphore约 15ms —— 差一倍不是夸张 - Linux Mono 下差距更明显:
Semaphore依赖 futex 模拟,但仍有额外封装开销 - ASP.NET Core 中用
Semaphore做限流,等于主动放弃 async/await 的调度优势,把异步变同步
容易被忽略的坑:计数器溢出与非重入性
Semaphore 的计数器允许超过初始最大值。比如 new Semaphore(5, 5),若连续调用 6 次 Release(),计数器变成 6,后续 6 个线程都能直接通过 WaitOne() —— 这不是 bug,是设计如此,但极易引发资源过载。
另一个硬伤是**非重入性**:同一个线程重复调用 WaitOne() 会导致永久阻塞(不会像 lock 那样识别同一线程递归进入)。你无法在单个线程里安全地“嵌套获取”,也没有 TryEnter 之类辅助方法。
所以,除非你真在写跨进程 IPC 层,否则别让 Semaphore 出现在业务代码里。它的存在意义,就是当 SemaphoreSlim 够不着的时候,兜底用的那把生锈但结实的扳手。


















