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

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

什么时候必须用
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
,而不是依赖“同一线程释放”的假设。

相关推荐