什么时候该用 Mutex
,而不是 SemaphoreSlim
只在需要跨进程同步时才选
Mutex——比如确保整个操作系统里只有一个程序实例运行,或多个独立进程(如 Windows 服务 + 桌面客户端)要协调访问同一份文件或共享内存。
SemaphoreSlim完全不能跨进程,它连操作系统句柄都不创建,纯用户态实现。 ✅ 正确场景:
Mutex用于单实例限制(命名互斥体)、进程间资源仲裁 ❌ 错误场景:仅在同一个进程内做线程同步,却用
Mutex——性能差 50 倍以上,且容易因未释放导致死锁 ⚠️ 注意:
Mutex必须由持有它的线程调用
ReleaseMutex(),否则其他线程永远等不到;而
SemaphoreSlim任意线程都能调用
Release()
SemaphoreSlim
的真实优势在哪
它不是“简化版信号量”,而是为高并发、短临界区和异步编程专门优化的同步基元。它在无竞争时完全不进内核,靠自旋+轻量队列处理;一旦有争抢,才可能升级到内核等待——这比
Semaphore或
Mutex每次都触发系统调用快得多。 ✅ 推荐场景:限制线程池并发数(如
Parallel.ForEachAsync控制最大并发 HTTP 请求)、生产者-消费者缓冲区计数、API 限流中间件 ✅ 异步友好:
WaitAsync()不阻塞线程,适合 ASP.NET Core 等 I/O 密集型服务 ❌ 不支持跨进程、不支持
Release(int)批量释放、不能替代独占锁(如写操作保护)
性能差距有多大?别靠猜
实测数据(
times = 0xFFFFF ≈ 104万次临界区进入)显示:
lock/
Monitor耗时约 1.3 秒;
SemaphoreSlim约 2.7 秒;
Mutex和
Semaphore则超过 13 秒——量级差异。这不是微优化,是选错原语直接拖垮吞吐量。 ? 关键区别:耗时主要来自系统调用开销。
Mutex每次
WaitOne()都进内核;
SemaphoreSlim默认只在竞争激烈时才进 ? 参数陷阱:
SemaphoreSlim(1, 1)看似等价于
Mutex,但行为不同——它没所有权概念,也无需同一线程释放 ? 安全底线:若临界区执行时间 > 1ms,优先考虑
SemaphoreSlim或
lock;若 > 100ms,应重构逻辑,而非硬扛锁
一个常见错误:把 SemaphoreSlim
当成“可重入锁”用
SemaphoreSlim不检查调用线程,也不记录谁获取了许可。这意味着:同一个线程反复
Wait()会把自己卡住(计数归零后无法再进),除非你手动
Release()多次——但它不会帮你记“进了几次”。这和
lock或
Monitor的可重入性完全不同。 ❌ 错误写法:
var sem = new SemaphoreSlim(1); sem.Wait(); // ✅ sem.Wait(); // ❌ 死等(除非其他线程 Release)✅ 正确做法:明确设计为“资源配额”,不是“代码段保护”。例如控制最多 5 个数据库连接同时活跃,就初始化为
new SemaphoreSlim(5, 5)? 提示:如果真需要可重入 + 异步支持,用
AsyncLock(社区封装)或
ReaderWriterLockSlim(读多写少时)
最常被忽略的一点:
Mutex的异常安全极难保障——
WaitOne()成功后若中途抛异常,
ReleaseMutex()很容易漏掉;而
SemaphoreSlim虽然没所有权,但至少不会因“忘记释放”导致全局阻塞——它只是让并发数暂时少一个。所以,宁可多花点时间封装
SemaphoreSlim的
using模式,也别裸写
Mutex。
