承接上一篇原子类型与自旋锁本篇聚焦线程间协作的核心工具条件变量。互斥锁解决了 共享数据竞争 问题而条件变量解决了 线程间等待 - 通知 问题 —— 让线程可以在某个条件不满足时主动阻塞等待直到其他线程通知条件成立。1为什么需要条件变量先看一个问题如何实现 线程 A 等待线程 B 完成某个任务后再继续执行错误方案轮训查询#include iostream #include thread #include mutex using namespace std; bool task_done false; mutex mtx; void worker() { // 模拟耗时任务 this_thread::sleep_for(chrono::seconds(2)); lock_guardmutex lg(mtx); task_done true; cout 任务完成 endl; } void waiter() { while (true) { lock_guardmutex lg(mtx); if (task_done) { break; } // 不加sleep会占满CPU // 加sleep会导致响应延迟 } cout 等待完成继续执行 endl; } int main() { thread t1(worker); thread t2(waiter); t1.join(); t2.join(); return 0; }轮询的致命问题CPU 浪费线程不断循环检查条件即使条件不满足也会占用 CPU响应延迟如果加了 sleep条件成立后线程不能立即被唤醒锁竞争频繁加锁解锁增加了锁的竞争开销条件变量就是为了解决这个问题而生让线程在条件不满足时主动阻塞休眠释放 CPU当条件成立时由其他线程主动唤醒等待的线程。2std::conditon_variable1基本原理条件变量必须配合互斥锁一起使用工作流程如下等待线程先获取互斥锁检查条件是否满足如果条件不满足调用wait()原子地释放锁并阻塞当前线程通知线程获取互斥锁修改条件然后调用notify_one()或notify_all()唤醒等待线程等待线程被唤醒后原子地重新获取互斥锁再次检查条件是否满足关键wait()的 释放锁 阻塞 和 唤醒 重新加锁 都是原子操作不会产生竞态条件。2核心接口// 等待阻塞当前线程直到被通知或虚假唤醒 void wait(unique_lockmutex lck); // 带谓词的等待阻塞直到被通知且谓词返回true template class Predicate void wait(unique_lockmutex lck, Predicate pred); // 唤醒一个等待的线程 void notify_one() noexcept; // 唤醒所有等待的线程 void notify_all() noexcept;3为什么必须用unique_lock不能用lock_guard课件里明确提到wait()只能接收unique_lockmutex类型的参数原因是wait()需要在阻塞前手动解锁互斥锁唤醒后手动重新加锁lock_guard不支持手动解锁和加锁而unique_lock支持unique_lock的灵活性正好满足条件变量的需求3条件变量的基本使用1最简单的等待-通知示例#include iostream #include thread #include mutex #include condition_variable using namespace std; mutex mtx; condition_variable cv; bool ready false; // 条件标志 void worker() { // 模拟耗时任务 this_thread::sleep_for(chrono::seconds(2)); // 修改条件必须加锁 lock_guardmutex lg(mtx); ready true; cout 任务完成通知等待线程 endl; // 通知等待线程 cv.notify_one(); } void waiter() { // 必须用unique_lock unique_lockmutex lck(mtx); // 等待条件成立 // 注意必须用while循环不能用if后面会讲为什么 while (!ready) { cv.wait(lck); // 阻塞释放锁 } cout 收到通知继续执行 endl; } int main() { thread t1(worker); thread t2(waiter); t1.join(); t2.join(); return 0; }2带谓词的wait推荐写法C11 提供了带谓词的wait()重载内部已经帮我们实现了 while 循环代码更简洁// 等价于上面的while循环 cv.wait(lck, [](){ return ready; });这是推荐的标准写法可以避免忘记写 while 循环导致的错误。4虚假唤醒1什么是虚假唤醒虚假唤醒是指即使没有线程调用notify_one()或notify_all()wait()也可能会随机返回。这不是 bug而是操作系统和硬件的特性 —— 某些平台的条件变量实现允许出现虚假唤醒以提高性能。2为什么必须用while循环如果用if而不是while来判断条件虚假唤醒发生时线程会误以为条件已经成立继续执行导致程序出错// 错误写法 if (!ready) { cv.wait(lck); // 可能被虚假唤醒 } // 虚假唤醒后ready仍然是false但程序会继续执行正确写法用while循环每次被唤醒后都重新检查条件// 正确写法 while (!ready) { cv.wait(lck); }或者使用带谓词的wait()cv.wait(lck, [](){ return ready; });记住永远不要在 if 语句中使用 wait ()必须用 while 循环或带谓词的重载。5经典示例1两个线程交替打印奇数和偶数1完整代码#include iostream #include thread #include mutex #include condition_variable using namespace std; int main() { mutex mtx; condition_variable cv; const int n 100; bool flag true; // true: 打印偶数线程执行false: 打印奇数线程执行 // 线程1打印偶数 0,2,4,...,98 thread t1([]() { int i 0; while (i n) { unique_lockmutex lck(mtx); // 等待flag为true cv.wait(lck, [](){ return flag; }); cout 偶数线程 i endl; i 2; flag false; // 切换到奇数线程 cv.notify_one(); // 通知奇数线程 } }); // 线程2打印奇数 1,3,5,...,99 thread t2([]() { int j 1; while (j n) { unique_lockmutex lck(mtx); // 等待flag为false cv.wait(lck, [](){ return !flag; }); cout 奇数线程 j endl; j 2; flag true; // 切换到偶数线程 cv.notify_one(); // 通知偶数线程 } }); t1.join(); t2.join(); return 0; }2工作原理t1 先启动t1 获取锁flag 为 true打印偶数设置 flag 为 false通知 t2t1 再次循环flag 为 false阻塞等待t2 被唤醒t2 获取锁flag 为 false打印奇数设置 flag 为 true通知 t1t2 再次循环flag 为 true阻塞等待循环往复两个线程交替执行直到打印完所有数字无论哪个线程先启动或者谁先抢到锁这个逻辑都能保证严格的交替打印。6经典示例2生产者-消费者模型生产者 - 消费者模型是条件变量最经典的应用场景生产者线程生产数据放入缓冲区消费者线程从缓冲区取出数据消费缓冲区满时生产者阻塞等待缓冲区空时消费者阻塞等待1完整代码#include iostream #include thread #include mutex #include condition_variable #include queue using namespace std; constexpr int BUFFER_SIZE 5; /// 缓冲区大小 queueint buffer; // 缓冲区 mutex mtx; condition_variable not_full; // 缓冲区非满 condition_variable not_empty; // 缓冲区非空 bool production_finished false; void producer(int id) { for (int i 0; i 10; i) { unique_lockmutex lck(mtx); // 缓冲区非满 not_full.wait(lck, []() { return buffer.size() BUFFER_SIZE; }); // produce data int data id * 100 i; buffer.push(data); cout produce: data ,buffersize: buffer.size() endl; // 通知消费者线程 not_empty.notify_one(); // move to next step lck.unlock(); this_thread::sleep_for(chrono::milliseconds(100)); } } void consumer(int id) { for (;;) { unique_lockmutex lck(mtx); // 缓冲区非空 not_empty.wait(lck, []() { return buffer.size() 0 || production_finished; }); if (buffer.empty() production_finished) { break; } // consume data int data buffer.front(); buffer.pop(); cout consume: data ,buffersize: buffer.size() endl; // 通知生产者线程 not_full.notify_one(); lck.unlock(); this_thread::sleep_for(chrono::milliseconds(200)); } } int main() { thread producers[2]; thread consumers[3]; for (int i 0; i 2; i) { producers[i] thread(producer, i); } for (int i 0; i 3; i) { consumers[i] thread(consumer, i); } for (int i 0; i 2; i) { producers[i].join(); } // 设置生产完成标志 { unique_lockmutex lck(mtx); production_finished true; } not_empty.notify_all(); // 等待所有消费者线程完成 for (int i 0; i 3; i) { consumers[i].join(); } cout task done endl; return 0; }2关键设计点两个条件变量not_full用于生产者等待not_empty用于消费者等待提前解锁在模拟耗时操作前提前解锁减少锁的持有时间提高并发度谓词等待使用带谓词的wait()避免虚假唤醒优雅退出等待所有生产者完成再等待缓冲区为空然后退出程序7std::condition_variable_anystd::condition_variable_any是std::condition_variable的泛化版本区别在于std::condition_variable只能配合std::unique_lockstd::mutex使用std::condition_variable_any可以配合任何满足 BasicLockable 要求的锁使用包括std::recursive_mutexstd::timed_mutexstd::recursive_timed_mutex自定义锁类型自旋锁配合recursive_mutex使用#include condition_variable #include mutex recursive_mutex rmtx; condition_variable_any cv_any; bool ready false; void wait_func() { unique_lockrecursive_mutex lck(rmtx); cv_any.wait(lck, [](){ return ready; }); }condition_variable_any的接口和condition_variable完全相同只是灵活性更高但性能略低。如果只需要配合std::mutex使用优先选择std::condition_variable。8超时等待条件变量还支持超时等待避免线程无限阻塞// 等待指定时长超时返回cv_status::timeout template class Rep, class Period cv_status wait_for(unique_lockmutex lck, const chrono::durationRep, Period rel_time); // 等待到指定时间点超时返回cv_status::timeout template class Clock, class Duration cv_status wait_until(unique_lockmutex lck, const chrono::time_pointClock, Duration abs_time); // 带谓词的超时等待超时返回false template class Rep, class Period, class Predicate bool wait_for(unique_lockmutex lck, const chrono::durationRep, Period rel_time, Predicate pred);示例unique_lockmutex lck(mtx); // 最多等待1秒 if (cv.wait_for(lck, chrono::seconds(1), [](){ return ready; })) { cout 条件成立继续执行 endl; } else { cout 等待超时 endl; }9notify_one和notify_all函数作用适用场景notify_one()唤醒一个等待的线程只有一个线程能处理条件成立的情况notify_all()唤醒所有等待的线程多个线程都能处理条件成立的情况惊群效应当调用notify_all()时所有等待的线程都会被唤醒但只有一个线程能获取到锁其他线程会再次阻塞。这种现象称为惊群效应会导致不必要的上下文切换和锁竞争影响性能。避免惊群效应的原则只有当多个线程都能处理条件成立的情况时才使用notify_all()否则优先使用notify_one()10总结条件变量用于实现线程间的等待 - 通知机制解决了轮询的 CPU 浪费和响应延迟问题条件变量必须配合互斥锁使用且只能接收unique_lock类型的参数虚假唤醒是条件变量的固有特性必须用 while 循环或带谓词的 wait () 来处理交替打印和生产者 - 消费者是条件变量最经典的两个应用场景condition_variable_any可以配合任意锁类型使用灵活性更高但性能略低超时等待可以避免线程无限阻塞notify_one()优先于notify_all()以避免惊群效应