必须用Semaphore仅当跨进程同步,如Web API与后台服务共享硬件设备;SemaphoreSlim适用于单进程内高性能异步限流,支持CancellationToken和超时控制,性能更优。

什么时候必须用 Semaphore?
只有当你需要跨进程同步时,才非用 Semaphore 不可。比如两个独立的 .NET 进程(如 Web API 和后台服务)要共享同一个硬件设备、全局日志文件或数据库连接池,就得靠命名信号量协调。
-
Semaphore底层包装 Win32 内核对象,支持通过名称(如"MySharedResource")在系统范围内暴露,另一进程可用Semaphore.OpenExisting("MySharedResource")打开它 - 它不支持
WaitAsync(),所有等待都是同步阻塞,WaitOne()会吃掉线程池线程——在 ASP.NET Core 等高并发异步场景中直接禁用 - 每次
WaitOne()或Release()都触发用户态→内核态切换,性能开销明显,不适合高频、短等待场景
SemaphoreSlim 是你日常该用的默认选择
95% 的 C# 并发限流、资源池控制(如 HTTP 客户端并发数、数据库连接数)都该用 SemaphoreSlim,它专为单进程内高性能异步设计。
- 构造时传入两个参数:
new SemaphoreSlim(initialCount, maxCount)—— 注意initialCount可以小于maxCount,比如new SemaphoreSlim(2, 5)表示初始放行 2 个线程,后续还能动态“补发”最多 3 个许可 - 必须用
await _sem.WaitAsync(cancellationToken),别写WaitOne();释放务必放在finally块里:try { ... } finally { _sem.Release(); } - 它不支持命名,不能跨进程;但支持
CancellationToken,能响应超时和取消,这对 Web 请求、定时任务很关键
常见死锁/异常怎么快速定位?
两类错误最典型:一种是“永远等不到”,一种是 SemaphoreFullException。
- “永远等不到”:大概率是
WaitAsync()没配TimeSpan或CancellationToken,上游调用方已超时放弃,而你还在死等 —— 始终给WaitAsync(TimeSpan.FromSeconds(30))加超时 -
SemaphoreFullException:说明Release()调用次数超过了WaitAsync()成功次数,比如异常路径漏了finally,或同一请求里多次Release()—— 检查所有Release()是否严格配对且只执行一次 - 别在
using里创建SemaphoreSlim实例,它是长期存活的协调器,不是一次性资源
性能差一倍?看底层等待方式
Semaphore 每次 WaitOne() 都走内核,哪怕只等 1ms,也要付出上下文切换成本;SemaphoreSlim 默认先自旋几十纳秒,抢到就走,没抢到才退化为内核事件 —— 这就是它快的本质。
- 实测:在 4 核 CPU 上模拟 1000 次短临界区访问,
SemaphoreSlim耗时约 8ms,Semaphore约 15ms,差距随竞争加剧而扩大 - 但若等待时间普遍 > 10ms(比如等外部 API),两者差异收敛,此时选型应由“是否跨进程”决定,而非性能
-
SemaphoreSlim的WaitHandle属性是延迟初始化的,仅当你显式访问(如用于WaitAny)才创建内核对象,平时零开销
真正容易被忽略的是:你根本不需要手动管理“谁该释放”。两种信号量都不绑定线程身份,Release() 可由任意线程调用 —— 这既是灵活性来源,也是 bug 温床。务必把 Release() 放进 finally,而不是依赖“同一线程释放”的假设。










