拓冰建站拓冰建站
首页 / 资讯中心 / 正文

【C++ 面试真题】31. 聊聊 C++ 的条件变量(condition_variable)

【C 面试真题】聊聊 C 的条件变量condition_variable消费者空转查队列是浪费“睡死了错过通知是 bug——条件变量就是这两个极端之间的正解。背得出wait 等通知只是及格真考你的是虚假唤醒为什么要防、丢失唤醒是怎么发生的、wait 为什么必须配 mutex、notify 到底要不要持锁”。本文把 condition_variable 讲透这三个坑各配一个翻车现场。一、开场条件变量解决什么问题❓ 为什么需要条件变量轮询不行吗✅ 条件变量解决等待某条件成立的睡眠与唤醒问题轮询busy-waitwhile(true) 查条件——空烧 CPU查的频率还两难快了浪费、慢了迟钝纯睡眠sleep 一小段再查——比轮询温和但通知到了还在睡延迟高条件变量wait原子地释放锁 睡眠notify一叫就醒——零空转、零延迟两头的优点全占。// 生产者-消费者的骨架condition_variable cv;mutex m;queueintq;// 消费者等条件unique_lockmutexul(m);cv.wait(ul,[]{return!q.empty();});// 生产者促成条件 通知{lock_guardmutexg(m);q.push(x);}cv.notify_one();二、推荐用法wait 带谓词❓ wait 的正确用法是什么✅推荐用带谓词的重载——一行顶安全三件套unique_lockmutexul(m);// 谓词版内部自动循环防虚假唤醒cv.wait(ul,[]{return!q.empty();// 条件});// 走到这里条件必然成立、锁也持有它等价于手写循环while(q.empty()){// ① 再查条件cv.wait(ul);// ② 条件不成立才睡}// 为什么必须循环// 醒来 ≠ 条件成立见下节醒来后自动重新持锁检查队列、取元素全程安全。两种写法可以自由选择谓词版是模板直调——标准规定它就等价于上面那个 while 循环没有额外的间接开销条件简单时一行搞定、意图清晰手写 while 循环则在条件复杂多行判断、需要中间变量或循环体内还要做别的事记日志、退避重试时更直观。选择看可读性不看性能。三、坑①虚假唤醒❓ 什么是虚假唤醒为什么要防✅没人 notifywait 也可能自己醒——这是操作系统底层机制futex/信号允许的行为标准明确规定 wait 返回不承诺被通知过。单次 if 判断的翻车现场// ❌ 危险写法unique_lockmutexul(m);if(q.empty())// 只判一次cv.wait(ul);// 醒来就往下走q.pop();// 虚假唤醒时// q 还是空的 → 崩醒来后条件可能根本不成立虚假醒来、或醒来到拿锁之间条件又被别人改掉所以必须回到循环再查——带谓词的 wait 把这个循环写进了库你只是看不见。 一句话if 只赌一次while 才是机制。谓词版 wait 就是库替你写的 while。四、坑②丢失唤醒❓ 丢失唤醒是怎么发生的✅ notify 发出时没人在等通知就 evaporate 了不排队不存档——之后才来的 wait 就傻等// ❌ 先 notify 后 wait 的时序灾难// 线程 1改条件 通知q.push(x);// 没加锁改的cv.notify_one();// 此刻没人在等// 通知蒸发// 线程 2稍后才开始等unique_lockmutexul(m);cv.wait(ul,[]{return!q.empty();});// 条件其实已经成立——// 幸好谓词先查直接通过 ✅看出来了救场的是谓词wait 入口先查条件条件已成立就不睡——通知丢不丢无所谓。这就是条件本身是事实通知只是提醒的设计哲学哪怕每个通知都丢只要条件会变、wait 总能靠查条件走出。⚠️ 反例改条件不持锁 只用无谓词 wait——双重失守死等没商量。五、坑③wait 为什么必须配 mutex❓ 条件变量为什么非要绑一把锁✅ 因为查条件和决定睡觉必须是原子的否则出现致命缝隙// 假设 wait 不持锁while(q.empty()){// ← 缝隙此刻条件变成成立// 且对方 notify 了没人在等蒸发cv.wait(ul);// 睡死再也没人叫}wait 的机制持 unique_lock 进来恰好补上这个缝两段语义都要讲清措辞对齐标准库文档wait(ul) 的两段关键语义 ① 进入时原子地[解锁 阻塞] —— 标准原话原子地调用 lock.unlock() 并阻塞于 *this —— 不存在已放锁却还没睡 的缝隙 ② 返回时解除阻塞后调用 lock.lock()拿到锁才返回 —— 解除阻塞有两个官方原因 被 notify或虚假唤醒 —— 加锁这一步本身也可能阻塞 锁暂时被别人握着 —— 后置条件返回时 lock.owns_lock() 必为 true这两条语义各防一头① 防丢失唤醒——挂起前锁还握着对方要改条件必须先拿锁、改完才能 notify通知不可能落进已放锁未挂起的缝里② 保证醒来即持锁——标准规定 wait 返回后lock.owns_lock()必为 true重新加锁若抛异常直接std::terminate使用者紧接着检查条件、操作共享数据天然安全不用自己再补一次加锁。解除阻塞的原因官方就列了两个——被通知或虚假唤醒——这正是第三节醒来必须循环查条件的根子。mutex 保护的是条件背后的共享数据条件变量负责高效的睡与醒——两者是一对缺一个都有缝。六、notify 的两个实务问题❓ notify_one 还是 notify_allnotify 要持锁吗✅ 两个高频追问一次答notify_one vs notify_all选择场景风险notify_one所有等待者等同一条件、唤醒一个干一个活任务队列唤醒的那个不一定能干活但谓词兜底notify_all等待者等不同条件、或条件一变多人能干惊群全醒抢锁多数白醒notify 要不要持锁两派都行语义不同——// A锁外通知推荐默认{lock_guardmutexg(m);q.push(x);}cv.notify_one();// ① 不会把// 还睡着但马上醒的人// 和等锁的人一起卡住// B锁内通知{lock_guardmutexg(m);q.push(x);cv.notify_one();}// ② 被唤醒者// 立刻撞锁再睡锁外通知少一次无谓的锁交接但条件存在多变量、且醒来者会立刻再判复杂条件时锁内通知逻辑更简单。真正不能错的是改条件必须持锁通知只是锦上添花的选择。七、超时等待与替代品❓ 等太久怎么办有没有别的等法✅ 两手准备① 超时版 wait——等活干但别等死if(cv.wait_for(ul,500ms,pred)){// 条件在限时内成立}else{// 超时做点别的// 报告状态、检查退出标志}②[C20]替代品 semaphore / jthread——计数信号量 std::counting_semaphore 适合纯计数场景不需要复杂条件比mutex cv 计数器三件套简洁得多条件复杂的场景 cv 仍是主力。 工程上还有个经典套路退出标志 notify_all——线程池关闭时置 flag、广播唤醒所有等待者谓词查stop || !q.empty()全部优雅退出。条件变量的谓词里放多个条件有活 || 要退是设计常用手法。八、面试高频追问❓ Q1wait(ul) 和 wait(ul, pred) 的区别✅ 无谓词版醒来直接返回不保证条件成立谓词版内部等价while (!pred()) wait(ul);——醒来必先验条件不成立接着睡。新代码一律谓词版无谓词版只出现在醒来后要自己写复杂逻辑的定制场景。❓ Q2被 notify 的线程立刻拿到锁了吗✅ 没有。醒来后的流程是从等待队列挪到锁的等待队列——还要竞争锁拿到锁才从 wait 返回。这就是惊群的代价来源notify_all 唤醒 N 个N 个抢一把锁N-1 个白醒还可能接着睡。❓ Q3notify 时线程还没进 wait通知算丢吗✅ 算丢——通知不缓存。但只要改条件持锁 wait 用谓词就不构成 bugwait 入口先查条件已成立直接走不依赖通知。条件是事实通知只是敲门声。❓ Q4一个 condition_variable 能配多把不同的 mutex 吗✅ 不能。同一 cv 的所有 wait 必须用同一把 mutex标准要求违反是 UBUndefined Behavior。多个独立条件要分开一个条件一把锁一个 cv各管各的。❓ Q5wait_for 返回 false 后谓词还可能成立吗✅ 可能。超时返回 false 只代表时限内谓词未成立过醒来时再查一次 pred 也许已经变了——严谨代码对超时分支也别忘条件检查的最后一眼。❓ Q6条件变量和信号量semaphore怎么选✅ cv 表达等条件条件可以是任意谓词配共享数据semaphore 表达等名额纯计数无数据依附。任务队列取元素要判空、判退出——用 cv限流 N 并发——[C20]semaphore 一行搞定。九、总结速查表考点一句话结论定位等条件的睡眠/唤醒替代轮询标准姿势wait(ul, 谓词)虚假唤醒醒来 ≠ 条件成立必须循环丢失唤醒通知不缓存谓词兜底wait 两段语义进入原子放锁挂起返回必然持锁notify 时机改条件必持锁通知可锁外one vs all同一条件 one多条件 all惊群notify_all 全醒抢锁N-1 白醒超时wait_for 分支处理[C20]semaphore 管纯计数一句话回顾条件变量 等条件的高效睡眠/唤醒永远用谓词版 wait虚拟唤醒、丢失唤醒一次全防wait 必须配 mutex查条件和入睡要原子改条件持锁、通知可在锁外同一条件 notify_one、多条件 notify_all——条件本身才是事实通知只是敲门声。如果您觉得本篇内容对你有帮助欢迎点赞 、收藏 ⭐、转发 。下期是多线程篇收官——聊原子变量与无锁编程atomic 怎么做到无锁、memory_order 六级是什么、手写 spinlock 和 CAS 循环敬请关注
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门