C# 信号量Semaphore使用方法 C# Semaphore和SemaphoreSlim有什么区别

1次阅读

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

C# 信号量Semaphore使用方法 C# Semaphore和SemaphoreSlim有什么区别

什么时候必须用 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() 没配 TimeSpanCancellationToken,上游调用方已超时放弃,而你还在死等 —— 始终给 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),两者差异收敛,此时选型应由“是否跨进程”决定,而非性能
  • SemaphoreSlimWaitHandle 属性是延迟初始化的,仅当你显式访问(如用于 WaitAny)才创建内核对象,平时零开销

真正容易被忽略的是:你根本不需要手动管理“谁该释放”。两种信号量都不绑定线程身份,Release() 可由任意线程调用 —— 这既是灵活性来源,也是 bug 温床。务必把 Release() 放进 finally,而不是依赖“同一线程释放”的假设。

text=ZqhQzanResources