为什么Monitor.Wait()与Monitor.Pulse()调用时必须先持有对应锁?
为什么Monitor.Wait/Pulse要求调用前必须持有对应对象的锁
这个设计是并发领域经过长期验证的标准方案,和POSIX条件变量的设计逻辑完全一致,并非给开发者增加不必要的负担,反而是从底层规避了并发编程中最常见的「丢失唤醒(Lost Wakeup)」问题,同时匹配Wait/Pulse本身的执行逻辑。
1. Wait的执行逻辑本身就要求调用方已持有锁
Monitor.Wait(obj)的核心执行步骤是固定的:
- 自动释放当前线程持有的
obj锁,让其他线程可以获取该锁修改共享状态 - 线程进入
obj的等待队列阻塞,直到收到Pulse/PulseAll信号 - 被唤醒后,重新竞争获取
obj锁,获取成功后才会返回
如果调用前没有持有obj锁,第一步的释放锁操作根本无法执行,这是最直接的底层逻辑限制。
2. 锁是保护「等待条件」的必须要素
所有调用Wait的场景,必然伴随对共享状态的条件判断,比如:等待队列不为空、等待计数器达到阈值等。这些共享状态本身就需要锁来保证读写的线程安全:
- 如果锁被封装到
Wait内部,那么你在调用Wait之前做的条件判断(比如if(queue.Count == 0))根本没有被锁保护,判断结果本身就可能是过期的 - 更严重的是会出现丢失唤醒问题:你判断完队列为空,还没调用
Wait的间隙,生产者线程已经往队列塞了数据并发送了Pulse,等你真正调用Wait时,这个信号已经被错过,线程会永远阻塞下去。
正确的用法必须是条件判断和Wait放在同一个锁的保护范围内:
lock (syncObj) { // 条件判断和Wait在同一个锁保护下,不会出现间隙被其他线程修改的问题 while (不满足执行条件) { Monitor.Wait(syncObj); } // 安全操作共享状态 }
3. Pulse要求持有锁也是为了避免并发错误
Pulse的作用是通知等待线程「共享状态已经变化,可以检查条件了」:
- 发送
Pulse的时机,必然是你已经修改了共享状态之后,而修改共享状态本身就需要持有锁保证线程安全 - 如果允许不持有锁就发
Pulse,可能出现状态修改还没对其他线程可见、Pulse就已经发出的问题,或者出现Pulse发送在Wait进入等待队列之前的丢失唤醒问题。
4. 为什么不解耦锁和Wait/Pulse
Wait/Pulse的设计定位就是配合锁使用的条件同步原语,而非通用的信号通知工具:
- 如果你需要不需要锁的信号通知,可以直接用
Semaphore/AutoResetEvent等同步类,它们的设计目标就是无锁关联的信号传递 - 强制绑定锁的规则,相当于从API层面帮开发者规避了90%以上的条件同步错误,毕竟丢失唤醒这类问题没有明显报错,排查难度极高,提前通过异常报错反而能降低整体开发成本。
官方文档里要求不在同步块内调用就抛SynchronizationLockException,本质是提前拦截错误用法,避免出现更难排查的死锁、逻辑异常问题。
内容的提问来源于stack exchange,提问作者Jake1234
相关产品推荐
相关产品推荐

