第4章 并发编程与多线程:线程同步、锁机制、条件变量、线程安全设计
并发编程是 C++ 服务端面试核心考点,直接考察候选人是否具备多线程协同、资源竞争控制、死锁预防等实战能力。本章从基础同步原语到真实项目案例,系统拆解面试高频题型。
常见埋点:锁的选择(mutex vs rwlock vs spinlock)、条件变量虚假唤醒、线程安全容器设计、死锁定位与预防。
面试问答卡片
📌 问题1:std::mutex 和 std::recursive_mutex 的区别?你会在什么场景下使用后者?
| 考察点 | 是否理解递归锁的设计动机与性能代价;能否避免滥用。 |
| 回答框架 | ① 定义区别:recursive_mutex 允许同一线程多次 lock,但需要对应次数 unlock。② 性能:recursive_mutex 维护计数,性能略低于普通 mutex。③ 场景:递归函数或回调中需要重入获取锁;但更推荐重构代码避免递归加锁。 |
| 示例回答 | “普通 mutex 不可重入,递归锁可同一线程多次加锁。我通常避免使用,因为可能是设计缺陷;只有在递归算法中无法避免时才使用,并确保 unlock 次数匹配。” |
| 可能追问 | 如何用普通 mutex 实现递归锁? |
| 避坑提醒 | ❌ 不要用递归锁掩盖加锁顺序混乱;面试时优先给出重构方案再提递归锁。 |
📌 问题2:条件变量为什么需要配合互斥锁使用?简述 wait 和 notify_one 的工作机制。
| 考察点 | 是否理解条件变量的原子 unlock + wait 操作;虚假唤醒处理。 |
| 回答框架 | ① 条件变量必须关联一个 mutex 以保证条件检查的原子性。② wait 内部先 release 锁,挂起线程,被唤醒后重新 acquire 锁。③ notify_one 只唤醒一个等待线程,不释放锁。④ 必须用 while 循环检查条件防虚假唤醒。 |
| 示例回答 | “wait 会在检查条件前释放锁、阻塞线程,被唤醒后再加锁。这样生产者 notify 时消费者能获取锁。我每次都使用 while(!pred) cv.wait(lock) 写法,避免虚假唤醒。” |
| 可能追问 | notify_all 和 notify_one 的区别?什么时候用 all? |
| 避坑提醒 | ❌ 不要在已经持有锁的情况下调用 notify,否则可能唤醒后立即因锁竞争而休眠;最佳实践是在解锁后 notify。 |
📌 问题3:如何设计一个线程安全的队列?(不采用现有库)
| 考察点 | 锁的粒度、条件变量使用、异常安全、move 语义优化。 |
| 回答框架 | ① 内部持有一个 std::deque 和 std::mutex。② push 加锁后放入数据。③ pop 用条件变量 waiting,加锁后检查是否为空。④ 返回时使用 move 避免拷贝。⑤ 提供 try_pop 返回 optional 避免阻塞。 |
| 示例回答 | “我会用 mutex 保护 deque,条件变量让 pop 等待。push 用 lock_guard,pop 用 unique_lock 配合 cv.wait。返回 T 时用 std::move 优化。同时提供 try_pop 支持非阻塞获取。” |
| 可能追问 | 如果队列是多个 producer 多个 consumer,需要做什么调整? |
| 避坑提醒 | ❌ 不要在 pop 函数中返回数据后再解锁,否则临时变量拷贝时仍持锁影响并发;应移动构造临时对象后尽早解锁。 |
项目案例模板
| 项目名称 | 实时监控数据聚合服务 |
| 业务背景 | 从数百台服务器采集 CPU/内存指标,按 5 秒窗口聚合后写入时序数据库。 |
| 规模指标 | QPS 10万+,聚合窗口 2000 个 bucket,线程数 8。 |
| 技术栈 | C++17, std::thread, std::shared_mutex, lock-free ring buffer。 |
| 核心难点 | 高频写入时共享 bucket 的竞争;读写锁如何平衡读(聚合)与写(更新)的公平性;防止写线程饥饿。 |
| 面试高频追问 | 为什么不用互斥锁?如何保证聚合时读到的数据一致?读写锁的性能开销? |
| 可迁移回答话术 | “我们采用 shared_mutex 实现读多写少场景的并发优化。写线程加独占锁更新 bucket 数组,聚合线程加共享锁批量读取快照。同时通过双缓冲机制减少锁持有时间。” |
大厂模拟面试
面试官提问:“请描述 std::condition_variable 的 notify_one 和 notify_all 的区别,并说说你如何在生产级代码中选择使用它们?”
候选人回答:(候选人准备作答)
普通答案:“notify_one 唤醒一个线程,notify_all 唤醒全部。我一般用 notify_one,因为性能好。如果多个线程都要处理那就用 notify_all。”
❌ 缺乏场景分析和细节。
优秀答案:“notify_one 唤醒某个等待线程(具体哪个由调度决定),notify_all 唤醒所有。选择依据:如果只有一个线程能处理事件(如任务队列 pop),用 notify_one 避免惊群;如果所有线程都可能需要重新检查条件(如资源可用性变化),用 notify_all。我还会在 notify_one 之前提前解锁,降低唤醒后锁竞争。”
✅ 有原理 + 场景决策 + 优化技巧。
专家答案:“我补充三个生产实践:1)使用 notify_all 时需确保线程醒来后用 while 循环检查条件,防止虚假唤醒;2)在持久化线程池中,我优先使用 notify_one,因为任务分发通常只需一个 worker;3)使用 notify_one 后若发现条件仍为 true,可能丢失唤醒,因此我采用‘条件变量 + 计数’模式:每次 push 后 count++,消费者 wait 时检查 count>0。这样 notify_one 总能匹配一个事件。”
✅ 深度细节 + 计数模式 + 防丢失唤醒。
面试官追问:“如果使用 notify_one,如何保证高并发下不会有消费者永远等待(丢失唤醒)?请给出代码草图。”
(追问考察是否真的理解丢失唤醒问题以及解决方案)