“初值为零时,信号量还可表达事件交接:消费者的 在事件发生前等待,生产者执行 后留下一个可消费的许可。这里通知具有可计数状态;没有等待者时执行的 不会凭空消失,而是使后来的一个 直接完成。这一…”
形式陈述 ​
条件变量 wait(cv, m) 的关键语义是把调用者加入
这项原子性关闭了丢失唤醒窗口。若程序先普通 unlock(m),再另行 sleep(),修改者可能恰在两步之间令 wait 让“已经登记”与“锁已交给修改者”之间不存在这种空隙。
notify_one(或 signal)使某个等待者有资格醒来,notify_all(或 broadcast)使所有当前等待者有资格醒来。在常见 Mesa 语义下,通知者继续运行并持有锁;被唤醒线程只进入重新竞争 wait 返回。共享谓词可能在这段时间被别的线程改变,接口也可能允许虚假唤醒,所以标准协议必须是
lock(m)
while not P(shared_state):
wait(cv, m)
use_or_update(shared_state)
unlock(m)
Hoare monitor 语义会把控制权立即交给被通知线程,使它在通知瞬间可依赖谓词;这是另一套调度契约。现代 POSIX 与 C++ 风格接口通常按 Mesa 式循环推理,不能把两者的结论混用。无论哪种语义,条件变量都不承诺 FIFO、公平或无饥饿,这些属于等待队列和调度器的附加性质。
直觉 ​
mutex 只回答“谁可以查看和修改共享状态”,条件变量回答“状态暂时不合用时,怎样把锁交出去并等它可能改变”。等待者不是在等一个神秘信号,而是在等受保护谓词,例如“队列非空”。通知只是提示重新检查,并不替代谓词本身。
这也解释了为何等待必须释放锁。消费者若发现队列为空后仍持锁睡眠,生产者就永远无法取得锁来放入元素;把检查、登记等待和释放锁组织成同一协议,才让状态改变者有机会推进,又不漏掉恰好发生在交接边界上的变化。
例子与边界 ​
容量为 not_empty := size > 0 与 not_full := size < C。消费者持锁检查 not_empty,不成立便在对应条件变量上等待;取走元素后通知 not_full。生产者对称地等待 not_full,插入后通知 not_empty。共享队列、大小和谓词都在同一把锁下读写,通知则表示“相关状态已经改变,某类等待者值得重查”。
消费者被唤醒时不能直接假定队列非空。通知者释放锁后,另一个消费者可能先取得锁并取走唯一元素;或者运行库允许线程在没有对应通知时返回。若用 if 检查一次,后来的消费者会在空队列上继续;while 循环在重新取得锁后恢复谓词不变量,覆盖竞争与虚假唤醒两种边界。
普通条件变量也不是持久事件。若生产者在没有等待者时调用 notify_one,这个通知通常不会为未来线程积攒;未来线程必须依据共享状态判断是否需要等待。若需求确实是保存一份可消费通知,初值为零的信号量更直接;若需求是永久记录“初始化完成”,共享布尔谓词才是事实来源。
推论与应用 ​
条件变量把共享状态、互斥与调度等待组合成可证明的条件同步协议。证明重点不是“发了多少次通知”,而是每次访问谓词时是否持锁、改变谓词后是否让相关等待者有机会重查,以及等待者醒来后是否用循环恢复前提。
notify_one 可减少惊群,却要求一次状态变化至多允许一个等待者取得有用进展;notify_all 适合全局阶段改变,但会让许多线程重新争锁。选择哪一个取决于谓词变化的语义。即便通知策略正确,线程最终被服务仍需单独的公平假设,条件变量的基本安全协议不提供这项保证。
参考资料
- ISO/IEC 14882, C++ 标准库
condition_variable规范。 - The Open Group, POSIX Threads, “Condition Variables”。
- Butler W. Lampson and David D. Redell, “Experience with Processes and Monitors in Mesa,” Communications of the ACM 23(2), 1980, pp. 105–117。