C++扩展 --- 并发支持库(补充1)https://rosetea.blog.csdn.net/article/details/149642554?spm=1001.2014.3001.5502

condition_variable 使用说明

本质还是封装了各个平台提供的条件变量的库,封装成面向过程的!

条件变量是用于两个线程之间的同步操作,比如我做了某一些事,然后进入阻塞了,你完成了某个事,再通知我 --- 最经典的就是生产者消费者模型:

  • condition_variable 需要配合互斥锁系列使用,主要提供 waitnotify 系统接口。

  • wait 需要传递一个 unique_lock<mutex> 类型的互斥锁,是写死了,在 wait 之前就已经锁了wait 会阻塞当前线程直到被 notify。在进入阻塞的瞬间,会解开互斥锁,防止自己阻塞,导致其他线程获取不到锁,方便了其他线程获取锁,访问条件变量。当被其他线程 notify 唤醒时,它会同时尝试去获取到锁,再继续往下运行。

  • notify_one 会唤醒当前条件变量上等待的其中一个线程,使用时也需要用互斥锁保护。如果没有现成阻塞等待,它啥事都不做;notify_all 会唤醒当前条件变量上等待的所有线程,但是只有一个线程可以获取到锁,所以 notify_all 谨慎使用!

  • condition_variable_any 类是 std::condition_variable 的泛化。相对于只在 std::unique_lock<std::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_lock<std::mutex> lck(mtx); // 创建一个互斥锁的唯一锁对象
    while (!ready) { // 如果ready为false,表示线程需要等待
        cv.wait(lck); // 线程进入等待状态,释放互斥锁
        // 当条件变量被通知时,线程会自动重新获取互斥锁,并继续执行
    }
    std::cout << "thread " << id << '\n'; // 打印线程ID
}

// 通知线程开始执行的函数
void go() {
    std::unique_lock<std::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的值。如果readyfalse,子线程会通过条件变量cv进入等待状态并释放互斥锁。主线程在所有子线程启动后,通过cv.notify_all()唤醒所有等待的子线程,并将ready设置为true。子线程被唤醒后重新获取互斥锁并继续执行,打印自己的线程ID,然后退出。主线程等待所有子线程结束后程序结束。 

经典问题:两个线程交替打印奇数和偶数

分析通过条件变量和锁如何保证交替打印
情况 1:t1 先启动,t2 过会才启动(未启动或者还在排队)

t1 启动后:先获取锁,flagtrue,不会被条件变量阻塞,打印 i0flag 修改为 falsei 修改为 2,再用条件变量唤醒其他阻塞线程。但此时没有线程等待,循环继续。再次获取锁时,flag 刚被修改为 false,此时会阻塞在条件变量上并解锁。此逻辑保证了 t1 不会连续打印。

t2 开始运行:先获取锁,flagt1 修改为 false,所以 t2 不会被条件变量阻塞,打印 j1flag 修改为 truej 修改为 3,再用条件变量唤醒其他阻塞线程,t1 被唤醒。t1 被唤醒后,需要分配时间片排队执行,有以下两种情况:

  • 第一种t1 没有立即执行,t2 继续执行,获取锁,但 flagtrue,所以阻塞在条件变量并解锁。过会 t1 开始执行,flagtrue,不会被条件变量继续阻塞,打印 2,继续上述循环逻辑,实现交替打印。

  • 第二种t1 立即执行,抢占到锁,flagtrue,不会被条件变量继续阻塞,打印 2i 修改为 4flag 修改为 false,再用条件变量唤醒其他阻塞线程。但此时没有线程被阻塞,继续循环逻辑,进入 t1t2 新一轮谁先执行或抢到锁资源的逻辑,实现交替打印。

情况 2:t2 先启动,t1 过会才启动(未启动或者还在排队)

t2 启动后:先获取锁,flagtrue,会被条件变量阻塞,并且同时解锁。

t1 开始运行:获取到锁资源,flagtrue,不会被条件变量阻塞,打印 i0flag 修改为 falsei 修改为 2,再用条件变量唤醒阻塞线程 t2t2 被唤醒后,也需要分配时间片排队执行,有以下两种情况:

  • 第一种t2 没有立即执行,t1 继续执行循环,获取锁,但 flagfalse,所以阻塞在条件变量并解锁。过会 t2 开始执行,flagfalse,不会被条件变量继续阻塞,打印 1j 修改为 3flag 修改为 true,唤醒阻塞线程 t1,进入上述类似逻辑,循环往复,实现交替打印。

  • 第二种t2 立即执行,抢占到锁,flagfalse,不会被条件变量继续阻塞,打印 1j 修改为 3flag 修改为 true,唤醒其他阻塞线程。但此时没有线程被阻塞,继续循环逻辑,进入 t1t2 新一轮谁先执行或抢到锁资源的逻辑,实现交替打印。

情况 3:t1 和 t2 几乎同时启动

本质:两个线程抢夺锁资源,t1 先抢到就类似情况 1,t2 先抢到就类似情况 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_lock<mutex> 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_lock<mutex> 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; // 程序结束
}

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐