什么时候该用 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根本无效
真正复杂的地方不在语法,而在于想清楚:你锁的到底是一个“门禁权限”,还是一批“限量门票”。混淆这两者,代码可能跑得通,但会在高并发或异常路径下突然卡死、数据错乱,而且极难复现。
