该用 Semaphore 而非 Mutex 的核心判断是:需控制“最多 N 个线程同时执行”时选 Semaphore,需“仅一个线程进入”时才考虑 Mutex;Semaphore 是无所有权的配额计数器,Mutex 是有所有权的独占锁。

什么时候该用 Semaphore,而不是 Mutex
核心判断就一条:你要控制的是“最多 N 个线程同时干活”,还是“必须只有一个线程能进”。前者选 Semaphore,后者才考虑 Mutex(或更轻量的 lock)。
Mutex 是“独占锁”——它只认“谁拿了,谁放”,且只允许一个线程持有;Semaphore 是“配额计数器”——它不记谁拿了,只管还剩几个“入场券”。比如数据库连接池设为 10,第 11 个线程调用 WaitOne() 就得排队,不管前 10 个是谁。
- 需要跨进程互斥(如确保程序只启动一个实例)→ 必须用命名
Mutex,Semaphore不行 - 要限制并发 HTTP 请求不超过 5 个 → 用
Semaphore(5, 5),别碰Mutex - 保护一个全局
int counter被多线程累加 → 直接用lock或Interlocked.Increment,Mutex太重,Semaphore语义错
Semaphore 的典型用途和构造参数怎么填
Semaphore 的两个构造参数:初始可用数(initialCount)和最大容量(maximumCount),不是“当前用了几个”或“总共创建几个”,而是“一开始发几张票”和“最多能发多少张票”。
常见误写:new Semaphore(0, 10) —— 这等于开门就拒客,所有线程一来就阻塞,除非你后续主动 Release(10),否则永远卡死。
- 资源池场景(如连接池上限 8)→
new Semaphore(8, 8):起始全空闲,最多允许 8 并发 - 生产者-消费者缓冲区大小为 10 → 用两个信号量:
emptySlots = new Semaphore(10, 10)(空位),filledItems = new Semaphore(0, 10)(已填项) - 限流 API 调用(每秒最多 3 次)→ 配合
Timer或令牌桶,但底层可基于SemaphoreSlim实现轻量等待
Semaphore 和 Mutex 最容易踩的坑
最危险的区别在“所有权”:Mutex 只能由获取它的线程调用 ReleaseMutex(),否则抛 ApplicationException;而 Semaphore 任意线程都能调用 Release() —— 这既是灵活性来源,也是 bug 温床。
- 误在非持有线程释放
Mutex→ 程序崩溃或静默失败(尤其在异步/线程池回调中) - 忘记
Release()导致Semaphore一直被扣光 → 后续所有线程永久挂起,无异常提示 - 用
Semaphore(1, 1)替代Mutex→ 功能看似一样,但失去所有权检查,调试时难定位谁没释放 - 跨进程场景硬上
Semaphore→ 它不支持跨进程同步,命名无效,会静默创建多个独立实例
性能与替代选择:什么时候该换 SemaphoreSlim
如果你的场景纯属进程内(in-process)、高频率争抢(比如每毫秒几十次 WaitOne),Semaphore 的内核态开销明显高于 SemaphoreSlim —— 前者每次调用都涉及系统调用,后者优先用自旋+用户态等待。
但注意:SemaphoreSlim 不支持跨进程,也不支持等待超时时指定 CancellationToken 的某些高级行为(.NET 6+ 已补全),且不能用于 WaitHandle.WaitAll() 等 WaitHandle 组合操作。
- Web API 限流、内存缓存并发读写控制 → 优先
SemaphoreSlim - 需和
AutoResetEvent等一起做 WaitHandle 数组等待 → 必须用Semaphore - 日志写入器要防多进程同时写同一文件 →
Mutex(命名)才是正解,SemaphoreSlim根本无效
真正复杂的地方不在语法,而在于想清楚:你锁的到底是一个“门禁权限”,还是一批“限量门票”。混淆这两者,代码可能跑得通,但会在高并发或异常路径下突然卡死、数据错乱,而且极难复现。










