C++条件变量详解(一看就懂)

首先,我们先来认识一下条件变量。

条件变量是一种同步原语,通常用于在多线程编程中,使一个线程在特定条件满足之前等待,同时允许其他线程在该条件发生更改时通知等待的线程。
1. “等待”:当条件不满足时(例如,某个资源还未准备好),线程可以选择等待。调用条件变量的 wait 方法会将线程置于阻塞状态,并释放已获取的互斥锁,让其他线程有机会修改条件。
2. “通知”:当条件发生变化时(例如,资源已经准备好),线程可以通过条件变量来通知其他等待这个条件的线程。这可以通过 notify_one(唤醒一个等待线程)或 notify_all(唤醒所有等待线程)方法实现。
3. “重检”:当被通知并从 wait 返回时,线程应重新检查条件以确定其是否真正满足。这是因为存在所谓的"虚假唤醒",即线程可能在条件实际满足之前被唤醒。

‌‌‌‌  例如,我们可以使用条件变量来同步一个生产者线程和一个消费者线程。生产者线程负责生成数据并将其放入缓冲区,消费者线程则从缓冲区中取出数据进行处理。用条件变量可以使消费者线程在缓冲区为空(即数据不足)时等待,而在生产者线程向缓冲区添加数据后,消费者线程会被唤醒并开始处理新数据。

‌‌‌‌  条件变量通常与互斥锁一起使用以确保线程在检查条件和决定等待之间不会被打断,即这两个操作是原子的。同时,在修改条件(比如修改共享数据)时,一般也会使用互斥锁来保证数据一致性。

  1. 条件变量的要点
    1. 检查所等待的条件:在调用 wait 方法之前,我们通常会在一个循环中检查所等待的条件。循环的目的是防止虚假唤醒,即 wait 由于未知原因返回但条件并未满足。
    2. 使用正确的锁:在调用 waitnotify_onenotify_all 时,我们需要使用一个 unique_lock 来保护条件变量。在调用 wait 时,锁会被释放,使其他线程有机会修改条件。等 wait 返回时,锁会被重新获得。
    3. 避免死锁:在使用条件变量时,我们需要留意不要引入死锁。例如,如果两个线程都在等待对方发送信号,但它们都无法发送信号(例如,因为它们都在等待对方释放某个资源),那么就会发生死锁。
    4. 避免滞后唤醒:如果 signal 先于 wait 发生,则该信号会丢失。为了防止这种情况,我们可以在修改条件的同时调用 notify_onenotify_all。这样,如果有其他线程正在等待,它们将立即被唤醒。如果没有线程正在等待,那么这个通知将被忽视。
    5. 注意 notify_allnotify_one 的区别:notify_all 会唤醒所有等待的线程,而 notify_one 只唤醒一个。如果多个线程都在等待同一个条件,那么 notify_all 可能更合适。

这里我们就以消费者和生产者为例,进行代码上的深入讲解。

对于一般的生产者-消费者模型,生产者会产生数据供消费者使用。生产者需要在准备好数据的时候通知消费者,消费者通过判断是否已经有数据来决定要不要消费数据。

生产者和消费者的实现如下所示:


#include<bits/stdc++.h>
#include<mutex>
#include<unistd.h>
using namespace std;
vector<int> buffer;
mutex mtx;
bool dataIsReady = false;
void producer(){
    while (true) {
        sleep(2);
        std::unique_lock<std::mutex> lk(mtx);
        //随机生成10个数字
        for (int i = 0; i < 10; i++) {
            buffer.push_back(rand()%20);
        }
        dataIsReady = true;
    }
}
void consumer(){
    while (true) {
        sleep(2);
        std::unique_lock<std::mutex> lk(mtx);
        if (!dataIsReady) 
            continue;
        //输出10个数字之后清空数组
        for (int i = 0; i < buffer.size(); i++) {
            if (i) putchar(' ');
            cout << buffer[i];
        }
        cout << endl;
        buffer.clear();
        dataIsReady = false;
    }
}
int main(){
    srand(time(NULL));
    thread produc(producer);
    thread consum(consumer);
    produc.join();
    consum.join();
    return 0;
}

std::unique_lock<std::mutex> lk(mtx); 的作用是创建一个互斥锁的锁对象 lk,并在创建时自动锁定 mtx。这意味着在 lk 的作用域内,互斥锁 mtx 被锁定,确保同一时刻只有一个线程可以访问被保护的资源(如 buffer)。

有两个好处:

  1. 自动解锁:

    std::unique_lock 是一个 RAII(Resource Acquisition Is Initialization)类,意味着它会在作用域结束时自动释放锁。当 lk 超出其作用域时(如函数结束或块结束),lk 的析构函数会被调用,从而自动解锁 mtx。

  2. 避免手动解锁:

    这种设计避免了手动解锁可能带来的错误,比如忘记解锁或在解锁后再次访问共享资源。使用 std::unique_lock 可以使代码更加安全和易于维护。

这种写法可以正常工作,但是存在一个问题:每次consumer线程都会循环判断数据是否准备好,这个过程中线程不会让出资源,因此循环判断会带来不必要的开销。正因如此,才需要学习条件变量。


// condition_variable::notify_all
#include <iostream>           // std::cout
#include <thread>             // std::thread
#include <mutex>              // std::mutex, std::unique_lock
#include <condition_variable> // std::condition_variable
 
std::mutex mtx;
std::condition_variable cv;
bool ready = false;
 
void print_id (int id) {
    std::unique_lock<std::mutex> lck(mtx);
    while (!ready) cv.wait(lck);
    // ...
    std::cout << "thread " << id << '\n';
}
 
void go() {
    //修改ready标记,并通知打印线程工作
    std::unique_lock<std::mutex> lck(mtx);
    ready = true;
    cv.notify_all();
}
 
int main (){
    std::thread threads[10];
    // 创建10个线程,每个线程当ready标记为真时打印自己的id号
    for (int i=0; i<10; ++i)
        threads[i] = std::thread(print_id,i);
 
    std::cout << "10 threads ready to race...\n";
    go();                       // go!
 
    for (auto& th : threads) th.join();
 
    return 0;
}

父女水果问题

问题描述:
  父亲、母亲分别向一个果盘中放置一个水果。父亲放置苹果,母亲放置橘子。儿子专门等待果盘中的苹果。女儿专门等待果盘中的橘子。当果盘中准备好水果以后,儿子和女儿分别根据自己的需要拿走水果。
  这其实也是一个生产者和消费者的模型,使用条件变量实现如下:


#include<bits/stdc++.h>
#include<mutex>
#include<unistd.h>
using namespace std;
mutex plate_mtx;                //盘子互斥量
condition_variable plate_cv;    //条件通知
//三个条件标记
bool appleIsReady = false;      //苹果已准备好
bool orangeIsReady = false;     //橘子已准备好
bool plateIsEmpty = true;       //盘子已经空了
 
void father() {
    int cnt = 1;
    while (true) {
        sleep(1);
        unique_lock<mutex> lck(plate_mtx);
        plate_cv.wait(lck, []{return plateIsEmpty;});
        //如果盘子不空,则准备苹果
        cout << "[Father] : I prepared my " << cnt;
        switch (cnt) {
            case 1 : cout << "st ";break;
            case 2 : cout << "nd ";break;
            case 3 : cout << "rd ";break;
            default: cout << "th ";break;
        }
        cnt++;
        cout << "apple." << endl;
        //修改盘子标记和苹果标记
        plateIsEmpty = false;
        appleIsReady = true;
        plate_cv.notify_all();
    }
}
void mother() {
    int cnt = 1;
    while (true) {
        sleep(1);
        unique_lock<mutex> lck(plate_mtx);
        plate_cv.wait(lck, []{return plateIsEmpty;});
        //如果盘子不空则准备橘子
        cout << "\t[Mother] : I prepared my " << cnt;
        switch (cnt) {
            case 1 : cout << "st ";break;
            case 2 : cout << "nd ";break;
            case 3 : cout << "rd ";break;
            default: cout << "th ";break;
        }
        cnt++;
        cout << "orange." << endl;
        //修改盘子标记和橘子标记
        plateIsEmpty = false;
        orangeIsReady = true;
        plate_cv.notify_all();
    }
}
void son() {
    while (true) {
        sleep(1);
        unique_lock<mutex> lck(plate_mtx);
        plate_cv.wait(lck, []{return !plateIsEmpty && appleIsReady;});
        //当盘子不空且苹果已经准备好的情况下拿苹果,并修改标记
        cout << "\t\t[Son] : I get an apple! Thank you, dad!" << endl;
        plateIsEmpty = true;
        appleIsReady = false;
        plate_cv.notify_all();
    }
}
void daughter() {
    while (true) {
        sleep(1);
        unique_lock<mutex> lck(plate_mtx);
        plate_cv.wait(lck, []{return !plateIsEmpty && orangeIsReady;});
        //当盘子不空且橘子已经准备好的情况下拿橘子,并修改标记
        cout << "\t\t\t[daughter] : I get an orange! Thank you, mom!" << endl;
        plateIsEmpty = true;
        orangeIsReady = false;
        plate_cv.notify_all();
    }
}
 
int main() {
    //创建线程
    thread father_thread(father);
    thread mother_thread(mother);
    thread son_thread(son);
    thread daughter_thread(daughter);
    //join所有线程
    father_thread.join();
    mother_thread.join();
    son_thread.join();
    daughter_thread.join();
 
    return 0;
}

在这个代码中,因为每个线程中我们都设置了sleep,所以看起来他可能是按父亲-儿子-母亲-女儿这样的顺序进行的。如果将sleep删去的话,那其实在盘子为空的时候,父亲进程和母亲进程是有相同的概率实现的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值