C++ 扩展 --- 并发支持库(补充2)
C扩展 --- 并发支持库补充1https://rosetea.blog.csdn.net/article/details/149642554?spm1001.2014.3001.5502condition_variable使用说明本质还是封装了各个平台提供的条件变量的库封装成面向过程的条件变量是用于两个线程之间的同步操作比如我做了某一些事然后进入阻塞了你完成了某个事再通知我 --- 最经典的就是生产者消费者模型condition_variable需要配合互斥锁系列使用主要提供wait和notify系统接口。条件变量的核心作用只有两个1. 让线程停下来等待wait线程说条件没满足我先睡了别占 CPU2. 让别人唤醒等待的线程notify另一个线程说条件满足了快起来干活std::this_thread::yield()并不是让线程彻底退出 CPU 执行它只是临时让出当前的 CPU 时间片让给其他线程运行但它自己仍然处于就绪状态操作系统会立刻再次把它加入调度队列它会马上回来继续尝试抢锁、继续执行本质上还是在不断循环、不断占用 CPU 资源属于一直在 “空转等待”并没有真正休息而条件变量的wait才是真正让线程挂起休眠彻底退出 CPU 调度不再占用任何执行资源也不会参与抢锁直到其他线程调用notify唤醒它它才会重新恢复运行这也是wait能做到极低资源消耗而yield会大量浪费 CPU 的根本原因。std::this_thread::yield()线程让出 CPU →链入【就绪队列】→ 随时可以被 CPU 再次调度→一直在抢执行权一直在消耗 CPU条件变量 wait ()线程阻塞等待 →放入【等待队列】→完全脱离 CPU 调度→ 必须被 notify 唤醒才会回到就绪队列→不占 CPU不抢资源完全休息之前也说过yield 确实可以实现无锁编程它通过线程主动让出 CPU、不断循环重试的方式避免使用互斥锁但这种方式属于自旋等待只会在等待时间极短、锁竞争很小的场景下更快因为它避免了上下文切换的开销而条件变量的 wait 虽然会带来两次上下文切换成本但它会让线程真正休眠、完全不占用 CPU 资源在等待时间不确定、线程竞争激烈、需要长时间等待条件满足的场景下wait 不会像 yield 那样空转消耗 CPU资源利用率和系统效率都远远更高所以在我们平时写的多线程同步、线程等待唤醒这类需求里wait 几乎永远是更优选择yield 更多用于底层无锁数据结构、极短时间等待的特殊场景不适合常规线程同步。wait需要传递一个unique_lockmutex类型的互斥锁是写死了在 wait 之前就已经锁了。wait会阻塞当前线程直到被notify。在进入阻塞的瞬间会解开互斥锁防止自己阻塞导致其他线程获取不到锁方便了其他线程获取锁访问条件变量。当被其他线程notify唤醒时它会同时尝试去获取到锁再继续往下运行。notify_one会唤醒当前条件变量上等待的其中一个线程使用时也需要用互斥锁保护。如果没有现成阻塞等待它啥事都不做notify_all会唤醒当前条件变量上等待的所有线程但是只有一个线程可以获取到锁所以 notify_all 谨慎使用condition_variable_any类是std::condition_variable的泛化。相对于只在std::unique_lockstd::mutex上工作的std::condition_variablecondition_variable_any能在任何满足可基本锁定BasicLockable要求的锁上工作。示例代码condition_variable::notify_all#include iostream // 引入标准输入输出库 #include thread // 引入线程库 #include mutex // 引入互斥锁库 #include condition_variable // 引入条件变量库 std::mutex mtx; // 定义一个互斥锁用于保护共享变量 std::condition_variable cv; // 定义一个条件变量用于线程间的同步 bool ready false; // 定义一个共享变量用于控制线程的执行 // 线程函数打印线程ID void print_id(int id) { std::unique_lockstd::mutex lck(mtx); // 创建一个互斥锁的唯一锁对象 while (!ready) { // 如果ready为false表示线程需要等待 cv.wait(lck); // 线程进入等待状态释放互斥锁 // 当条件变量被通知时线程会自动重新获取互斥锁并继续执行 } std::cout thread id \n; // 打印线程ID } // 通知线程开始执行的函数 void go() { std::unique_lockstd::mutex lck(mtx); // 创建一个互斥锁的唯一锁对象 ready true; // 设置共享变量ready为true表示线程可以开始执行 cv.notify_all(); // 通知所有等待条件变量的线程 } int main() { std::thread threads[10]; // 定义一个线程数组用于存储10个线程 // 创建10个线程每个线程调用print_id函数 for (int i 0; i 10; i) threads[i] std::thread(print_id, i); std::cout 10 threads ready to race...\n; // 提示所有线程准备就绪 std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 主线程暂停100毫秒 go(); // 调用go函数通知线程开始执行 // 等待所有线程结束 for (auto th : threads) th.join(); return 0; // 程序结束 }这段代码通过条件变量和互斥锁实现了线程的同步。主线程创建了10个子线程每个子线程在启动后会尝试获取互斥锁并检查共享变量ready的值。如果ready为false子线程会通过条件变量cv进入等待状态并释放互斥锁。主线程在所有子线程启动后通过cv.notify_all()唤醒所有等待的子线程并将ready设置为true。子线程被唤醒后重新获取互斥锁并继续执行打印自己的线程ID然后退出。主线程等待所有子线程结束后程序结束。当然了这个代码确实完全体现不出条件变量的价值条件read true只是一个开关没有生产者 - 消费者、没有任务队列、没有数据依赖等待。条件变量真正的价值是线程等待某个数据 / 状态就绪而不是 “等一个开关”。我们先不急我们再写一个简单的代码经典问题两个线程交替打印奇数和偶数分析通过条件变量和锁如何保证交替打印情况 1t1先启动t2过会才启动未启动或者还在排队t1启动后先获取锁flag是true不会被条件变量阻塞打印i为0flag修改为falsei修改为2再用条件变量唤醒其他阻塞线程。但此时没有线程等待循环继续。再次获取锁时flag刚被修改为false此时会阻塞在条件变量上并解锁。此逻辑保证了t1不会连续打印。t2开始运行先获取锁flag被t1修改为false所以t2不会被条件变量阻塞打印j为1flag修改为truej修改为3再用条件变量唤醒其他阻塞线程t1被唤醒。t1被唤醒后需要分配时间片排队执行有以下两种情况第一种t1没有立即执行t2继续执行获取锁但flag为true所以阻塞在条件变量并解锁。过会t1开始执行flag为true不会被条件变量继续阻塞打印2继续上述循环逻辑实现交替打印。第二种t1立即执行抢占到锁flag为true不会被条件变量继续阻塞打印2i修改为4flag修改为false再用条件变量唤醒其他阻塞线程。但此时没有线程被阻塞继续循环逻辑进入t1和t2新一轮谁先执行或抢到锁资源的逻辑实现交替打印。情况 2t2先启动t1过会才启动未启动或者还在排队t2启动后先获取锁flag是true会被条件变量阻塞并且同时解锁。t1开始运行获取到锁资源flag是true不会被条件变量阻塞打印i为0flag修改为falsei修改为2再用条件变量唤醒阻塞线程t2。t2被唤醒后也需要分配时间片排队执行有以下两种情况第一种t2没有立即执行t1继续执行循环获取锁但flag为false所以阻塞在条件变量并解锁。过会t2开始执行flag为false不会被条件变量继续阻塞打印1j修改为3flag修改为true唤醒阻塞线程t1进入上述类似逻辑循环往复实现交替打印。第二种t2立即执行抢占到锁flag为false不会被条件变量继续阻塞打印1j修改为3flag修改为true唤醒其他阻塞线程。但此时没有线程被阻塞继续循环逻辑进入t1和t2新一轮谁先执行或抢到锁资源的逻辑实现交替打印。情况 3t1和t2几乎同时启动本质两个线程抢夺锁资源t1先抢到就类似情况 1t2先抢到就类似情况 2不再细节分析。示例代码两个线程交替打印奇数和偶数#include iostream // 引入标准输入输出流库 #include thread // 引入线程库 #include mutex // 引入互斥锁库 #include condition_variable // 引入条件变量库 using namespace std; int main() { std::mutex mtx; // 定义一个互斥锁用于保护共享变量 condition_variable c; // 定义一个条件变量用于线程间的同步 int n 100; // 定义打印的数字上限 bool flag true; // 定义一个标志变量用于控制线程的执行顺序 // 创建线程 t1用于打印偶数 thread t1([]() { int i 0; // 初始化偶数起始值为 0 while (i n) { // 当打印的数字小于上限时继续执行 unique_lockmutex lock(mtx); // 获取互斥锁 while (!flag) { // 如果 flag 为 false表示当前不是 t1 的执行时间 c.wait(lock); // t1 等待条件变量的通知同时释放互斥锁 } cout i endl; // 打印当前偶数 flag false; // 修改标志变量表示 t1 已完成当前轮次 i 2; // 偶数加 2准备下一轮打印 c.notify_one(); // 通知另一个线程 t2 可以执行 } }); // 创建线程 t2用于打印奇数 thread t2([]() { int j 1; // 初始化奇数起始值为 1 while (j n) { // 当打印的数字小于上限时继续执行 unique_lockmutex lock(mtx); // 获取互斥锁 while (flag) { // 如果 flag 为 true表示当前不是 t2 的执行时间 c.wait(lock); // t2 等待条件变量的通知同时释放互斥锁 } cout j endl; // 打印当前奇数 j 2; // 奇数加 2准备下一轮打印 flag true; // 修改标志变量表示 t2 已完成当前轮次 c.notify_one(); // 通知另一个线程 t1 可以执行 } }); t1.join(); // 等待线程 t1 完成 t2.join(); // 等待线程 t2 完成 return 0; // 程序结束 }生产者 — 消费者模型生产者生产数据消费者没有数据就睡觉wait生产者生产完通知消费者notify消费者醒来拿数据这才是条件变量的真正用途线程等待某个条件成立不成立就休眠不浪费 CPU。#include iostream #include thread #include mutex #include condition_variable #include queue using namespace std; mutex mtx; condition_variable cv; queueint q; // 任务队列 bool done false; // 消费者等待队列里有数据才消费 void consumer() { while (true) { unique_lockmutex lck(mtx); // 重点没有数据就休眠释放锁 cv.wait(lck, []{ return !q.empty() || done; }); if (done q.empty()) break; // 取数据 int x q.front(); q.pop(); cout 消费 x endl; } } // 生产者生产数据通知消费者 void producer() { for (int i 1; i 5; i) { this_thread::sleep_for(chrono::seconds(1)); lock_guardmutex lck(mtx); q.push(i); cout 生产 i endl; cv.notify_one(); // 唤醒消费者 } // 生产完关闭 lock_guardmutex lck(mtx); done true; cv.notify_all(); } int main() { thread t1(producer); thread t2(consumer); t1.join(); t2.join(); return 0; }当然了我们这里也是简单的介绍一下的具体的可以在项目中学习应用