一、条件变量(std::condition_variable)的核心原理
| 概念 | 说明 |
|---|---|
| 目的 | 实现线程之间的同步------一个线程等待"条件成立",另一个线程修改条件并发出通知。 |
| 组成 | 通常与 std::mutex 和共享状态变量配合使用(例如 bool ready 或队列)。 |
| 常用接口 | wait(lock, predicate)、notify_one()、notify_all()。 |
⚙️ 工作流程(简要)
-
等待线程:
wait(lock, predicate)- 持有锁 → 检查条件;
- 如果条件不满足 → 自动释放锁并睡眠;
- 被唤醒时 → 自动重新加锁 → 再检查条件。
-
通知线程:
notify_one()或notify_all()- 发信号唤醒等待线程,但不会释放锁;
- 被唤醒线程要重新获得锁后才能继续执行。
二、std::mutex、std::lock_guard、std::unique_lock 的区别
| 锁类型 | 特点 | 是否能用于 wait() |
是否能手动解锁 | 使用场景 |
|---|---|---|---|---|
std::mutex |
原始互斥锁 | ❌ | ✅(通过显式 lock/unlock) | 底层手动控制 |
std::lock_guard |
RAII 加锁解锁(构造加锁,析构解锁) | ❌ | ❌ | 简单、短作用域的临界区 |
std::unique_lock |
可手动 lock/unlock,支持延迟上锁 | ✅ | ✅ | 必须与 cv.wait() 配合使用 |
三、cv.wait() 与 cv.notify_*() 的行为区别
| 函数 | 是否释放锁 | 功能 |
|---|---|---|
cv.wait(lock) |
✅ 等待时自动释放锁;被唤醒后自动加锁 | 线程阻塞等待条件 |
cv.wait(lock, predicate) |
✅ 同上,并自动循环检查条件 | 更安全避免伪唤醒 |
cv.notify_one() |
❌ 只发信号,不释放锁 | 唤醒一个等待线程 |
cv.notify_all() |
❌ 唤醒所有等待线程 | 唤醒广播信号 |
⚠️
notify_*()不会释放锁!如果当前线程持锁并执行耗时逻辑,被唤醒的线程仍然拿不到锁 → 会出现"白唤醒"。
四、两种典型应用场景对比
| 对比项 | ZooKeeper 等待连接 | 生产者--消费者模型 |
|---|---|---|
| 共享资源 | bool is_connected(状态标志) |
队列 std::queue(数据缓冲区) |
| 锁类型 | std::lock_guard(作用域短) |
std::unique_lock(可解锁再上锁) |
| wait 作用 | 主线程阻塞等待连接成功 | 等待队列空或满 |
| notify 时机 | Watcher 回调后 notify_all() |
push 或 pop 后 notify_one() |
| 是否手动 unlock() | 否,自动析构释放 | 是,避免持锁时 notify 白唤醒 |
| 临界区复杂度 | 简单(修改标志) | 复杂(多步 push/pop + 业务逻辑) |
| 解锁策略 | 自动(作用域极短) | 手动(先 unlock 再 notify) |
五、为什么"持锁 notify" 会造成白唤醒
notify_one()只是发信号,不会释放锁;- 被唤醒的线程要重新加锁;
- 当前线程还没释放锁 → 对方线程又阻塞;
- 导致唤醒提前、执行延迟;
- 如果在临界区内还有耗时逻辑(日志、IO、sleep),会进一步拉低吞吐。
六、最佳实践总结(面试/代码标准写法)
✅ 1. 锁粒度尽可能小
只在访问共享数据(临界区)时上锁。
业务逻辑应放在锁外执行。
✅ 2. 等待时用 unique_lock + cv.wait(lock, predicate)
这样 wait 内部自动释放锁、自动循环检查条件。
✅ 3. 通知前释放锁
推荐两种写法:
cpp
// A. 手动 unlock
lock.unlock();
cv.notify_one();
// B. 缩小作用域
{
std::unique_lock<std::mutex> lock(mtx);
buffer.push(x);
}
cv.notify_one();
✅ 4. 对简单状态同步,可直接用 lock_guard
短作用域 + 简单标志变量时,无需手动 unlock:
cpp
{
std::lock_guard<std::mutex> lock(cv_mutex);
is_connected = true;
cv.notify_all();
} // 自动解锁
七、面试常见问题总结
| 问题 | 简答要点 |
|---|---|
cv.wait() 为什么要传入锁? |
让系统在 wait 内能自动释放并重新加锁,保证条件检查的原子性。 |
notify_one() 会释放锁吗? |
❌ 不会;它只发信号,锁仍由当前线程持有。 |
| 为什么要在 notify 前 unlock? | 防止被唤醒线程拿不到锁,造成白唤醒和吞吐下降。 |
lock_guard 和 unique_lock 区别? |
前者作用域固定、不可 unlock;后者灵活可多次 lock/unlock。 |
| 为什么 wait 要循环检查 predicate? | 防止伪唤醒(系统层面或其他条件触发导致的假唤醒)。 |
notify_one 和 notify_all 区别? |
唤醒一个 vs 唤醒所有等待线程。 |
| 什么时候不手动解锁? | 临界区极短、没有后续逻辑时(如 ZooKeeper 示例)。 |
| 为什么生产者消费者模型用 unique_lock? | 因为要与 cv.wait() 配合,且需要手动控制锁释放时机。 |
八、图形化总结(思维导图式)
条件变量机制
│
├── wait(lock, pred)
│ ├─ 自动释放锁
│ ├─ 被唤醒后重新加锁
│ └─ 循环检查条件
│
├── notify_one / notify_all
│ ├─ 不释放锁
│ ├─ 唤醒等待线程
│ └─ 建议先解锁再通知
│
└── 应用场景
├─ ZooKeeper 状态同步
│ ├─ lock_guard 短作用域
│ └─ 自动解锁
└─ 生产者消费者模型
├─ unique_lock + wait
├─ unlock → notify_one
└─ 缩小临界区提升吞吐
✅ 九、总结一句话
- ZooKeeper 等待连接: 短作用域锁,只同步状态,不需手动解锁。
- 生产者消费者模型: 复杂共享资源,notify 前应解锁,避免白唤醒、提升并发效率。
- 核心原则: "wait 自动解锁,notify 不会解锁;锁只保护数据,不保护逻辑。"