详细讲解条件变量
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>class ThreadPrinter {
private:int count;// 共享资源:当前数字int max;// 共享资源:最大数字bool turn; // true表示第一个线程打印,false表示第二个线程打印std::mutex mtx;// 保护以上三个共享资源的锁std::condition_variable cv;// 用于线程间通信的条件变量public:ThreadPrinter(int max) : count(1), max(max), turn(true) {}void print(int threadId) {while (count <= max) {// 获取锁 - 关键步骤!std::unique_lock<std::mutex> lock(mtx);// 从这里开始,当前线程独占了共享资源:其他线程无法访问这些变量 count max turn// 检查是否轮到当前线程while ((threadId == 1 && !turn) || (threadId == 2 && turn)) {//条件不满足,进入等待状态cv.wait(lock);/*
释放锁 mtx(让其他线程可以访问共享资源)将线程置于等待状态(不消耗CPU)将线程加入到条件变量的等待队列中当其他线程调用 cv.notify_all() 时:所有等待的线程被标记为可运行状态线程尝试重新获取锁获取到锁后,从 wait() 调用中返回*/}// 执行打印工作(此时持有锁)std::cout << "Thread " << threadId << ": " << count << std::endl;count++;// 切换控制权turn = !turn;// 通知另一个线程可以打印了 通知其他线程cv.notify_all();//锁在作用域结束时自动释放// lock 析构函数被调用 → mtx.unlock()}}
};int main() {ThreadPrinter printer(100);std::thread t1(&ThreadPrinter::print, &printer, 1);std::thread t2(&ThreadPrinter::print, &printer, 2);t1.join();t2.join();return 0;
}
1. 条件变量是什么?
条件变量是一种线程同步机制,允许线程在某个条件不满足时等待,当条件满足时被其他线程唤醒。
核心方法:
-
wait():让线程等待 -
notify_one():唤醒一个等待的线程 -
notify_all():唤醒所有等待的线程
2. cv.wait(lock) 的详细工作原理
cpp
std::unique_lock<std::mutex> lock(mtx); cv.wait(lock);
wait() 内部执行三个原子操作:
-
释放锁:让其他线程可以访问共享资源
-
进入等待状态:线程挂起,不消耗CPU
-
被唤醒后重新获取锁:继续执行后续代码
相当于:
cpp
// wait() 的伪代码实现
void condition_variable::wait(std::unique_lock<std::mutex>& lock) {// 1. 释放锁,让其他线程运行lock.unlock();// 2. 将当前线程加入等待队列,进入睡眠状态add_to_waiting_queue(current_thread);sleep_current_thread();// 3. 被唤醒后,重新获取锁lock.lock();
}
3. 为什么用 while 而不用 if?
这是条件变量使用中最关键的理解点!
使用 if 的问题:
cpp
// 错误的方式!
if ((threadId == 1 && !turn) || (threadId == 2 && turn)) {cv.wait(lock);
}
// 被唤醒后直接继续执行,不再检查条件
可能发生的问题:
-
虚假唤醒:线程可能在没有收到
notify的情况下被唤醒 -
条件变化:在等待期间,条件可能被其他线程改变
-
多个线程同时通过:多个等待的线程可能同时被唤醒
使用 while 的正确性:
cpp
// 正确的方式!
while ((threadId == 1 && !turn) || (threadId == 2 && turn)) {cv.wait(lock);
}
// 被唤醒后重新检查条件,确保真的轮到我了
while 循环的保护作用:
-
每次被唤醒都重新检查条件
-
如果条件仍不满足,继续等待
-
确保只有条件真正满足时才继续执行
4. 在这段代码中的具体工作流程
让我们通过具体的执行序列来理解:
初始状态:
cpp
count = 1, turn = true, max = 100
执行序列1:正常情况
text
时间点: t0 t1 t2 t3 t4 线程1: 获取锁 → 检查条件 → 打印1 → 设置turn=false → 通知 线程2: 等待锁 → 检查条件 → 等待 → 被唤醒 → 打印2
详细步骤:
-
线程1:获取锁,检查条件
(1==1 && !true) = false→ 不等待,直接打印 -
线程1:设置
turn = false,调用cv.notify_all(),释放锁 -
线程2:获取锁,检查条件
(2==2 && false) = false→ 不等待,直接打印 -
线程2:设置
turn = true,调用cv.notify_all(),释放锁
执行序列2:线程竞争情况
text
时间点: t0 t1 t2 t3 t4 t5 线程1: 获取锁 → 检查条件 → 打印1 → 设置turn=false → 通知 → 释放锁 线程2: 等待锁 → 获取锁 → 检查条件 → 等待 → 被唤醒 → 打印2
5. 虚假唤醒的实际例子
假设由于系统调度或硬件原因,线程2在没有收到通知的情况下被唤醒:
cpp
// 线程2的执行流程: 1. 获取锁 2. 检查条件: (2==2 && true) = true → 进入等待 3. 虚假唤醒!(没有收到notify,但被系统唤醒了) 4. 重新检查条件: (2==2 && true) = true → 继续等待 ✓ 5. 真正被线程1唤醒 6. 重新检查条件: (2==2 && false) = false → 开始打印 ✓
如果用 if,在第3步就会错误地开始执行打印操作!
6. notify_all() 的作用
cpp
cv.notify_all(); // 唤醒所有等待的线程
为什么要用 notify_all() 而不是 notify_one()?
在这个例子中,虽然只有一个线程在等待,但使用 notify_all() 更安全:
-
确保所有等待的线程都被唤醒
-
避免某个线程永远等待的情况
-
代码更健壮,易于扩展
7. 完整的条件变量使用模式
标准的使用模板:
cpp
// 等待线程
{std::unique_lock<std::mutex> lock(mtx);while (!condition_is_satisfied) { // 必须用whilecv.wait(lock);}// 执行需要同步的操作
}// 通知线程
{std::lock_guard<std::mutex> lock(mtx);// 修改条件condition_is_satisfied = true;cv.notify_all(); // 或 cv.notify_one()
}
8. 总结
为什么用 while 不用 if:
-
防止虚假唤醒:系统可能无故唤醒线程
-
条件重检查:确保被唤醒时条件真正满足
-
多线程安全:多个线程可能同时被唤醒
条件变量的核心思想:
-
等待时释放锁:让其他线程可以工作
-
被唤醒时重新获取锁:保证数据一致性
-
循环检查条件:确保条件真正满足
