C++多线程之旅-条件变量condition variables

目录

      • 前言
      • 传统方法
      • 条件变量
        • notify_all()¬ify_one()
        • wait_for()&wait_until()
      • 伪唤醒
      • 总结

前言

前一篇文章讲到了线程同步问题 ,但是有一种情况需要考虑:线程B需要线程A结束之后才能继续进行,但是如何来得知线程A已经结束了呢?没有学过线程之前可以使用标识符flag标识线程A是否结束,但是此时线程B需要一直去检查标识符flag。虽然空循环不会对线程有很大的影响,但是还有会一直占用线程。有没有一种机制,在线程A完成以后自动通知线程B然后线程B才载入运行。今天我们就来讲一下条件变量的问题。

举一个简单的例子,你在从家到学校的路上需要坐公交车,但是又很困想睡觉,却又怕睡过了。传统方式就是每2分钟起来一次,看是不是到站了,但是这样你就会睡不好。另外一种方式就是使用地图的提醒功能,快到到站的时候提醒你起来,这时你不用占用休息的时间。其实条件变量的作用就是闹钟 的作用而已。

传统方法

bool flag = true;

void fun_1(){
    while (flag){
        cout << "wait!!" << endl;
        this_thread::sleep_for(chrono::seconds(1));
    }
    cout << "线程B执行开始" << endl;
    cout << "线程B执行结束" << endl;
}

void fun_2(){
    cout << "线程A执行开始" << endl;
    this_thread::sleep_for(chrono::seconds (5));
    cout << "线程A执行结束" << endl;
    flag = false;
}

当线程A执行结束之后修改标识符,然后线程B的循环结束,开始执行。执行结果如下:
在这里插入代码片C++多线程之旅-条件变量condition variables_第1张图片
此时线程B一直占用着处理器资源,还有一种方式就是使用前面的同步的方法使用互斥元进行。源代码如下:

mutex mut;
void fun_1(){
    lock_guard<mutex> lock(mut);
    cout << "线程B执行开始" << endl;
    cout << "线程B执行结束" << endl;
}

void fun_2(){
    cout << "线程A执行开始" << endl;
    this_thread::sleep_for(chrono::seconds (5));
    cout << "线程A执行结束" << endl;
    mut.unlock();
}

int main(){
    thread t_1(fun_1);
    thread t_2(fun_2);
    mut.lock();
    t_1.join();
    t_2.join();
}

在主线程中先把互斥元锁住,然后在线程A执行完毕之后再将互斥元解锁,线程B继续执行。虽然这样的方式可以实现异步,但是需要在主线程中锁定互斥元。这样很容易出现以往导致线程死锁。
C++多线程之旅-条件变量condition variables_第2张图片
下面就来看看今天引入的新方法,条件变量。

条件变量

关键词为condition_variablecondition_variable_any,都包含在头文件中。前者仅限于和mutex一起合作,后者则可以和所有能够实现互斥的东西合作。

condition_variable有两类函数一些是用于等待的,一类是用于唤醒的。

用于等待的函数通常有三个:

  • wait():通常是两个参数,一个是互斥元,另一个是条件函数;当条件表达式为假时,释放互斥元进入阻塞状态,等待唤醒函数的唤醒。
  • wait_for():通常是两个参数,一个是互斥元,另一个是等待时间;直接阻塞直到被唤醒或被超时。
  • wait_until:通常是两个参数,一个是互斥元,另一个是等待时刻;直接阻塞直到被唤醒或被超时。

用于唤醒的函数通常也是两个:

  • notify_one():唤醒其中一个线程。
  • notify_all():唤醒所有线程。

下面就来用一用这些函数吧,

notify_all()¬ify_one()

mutex mut;
condition_variable condition;

void fun_1(){
    unique_lock<mutex> lock(mut);
    condition.wait(lock);
    cout << "线程B执行开始" << endl;
    cout << "线程B执行结束" << endl;
}

void fun_2(){
    cout << "线程A执行开始" << endl;
    this_thread::sleep_for(chrono::seconds (5));
    cout << "线程A执行结束" << endl;
    condition.notify_one();
}

执行结果如下:
C++多线程之旅-条件变量condition variables_第3张图片
当线程B执行到wait()函数时,释放lock的锁定,然后线程B进入阻塞状态,等待被唤醒。线程A执行到notify_one()时,唤醒一个线程,线程B被唤醒继续执行。

如果需要唤醒的线程多于一个的时候,会出现什么情况呢?

    thread t_1(fun_1);
    thread t_2(fun_2);
    thread t_3(fun_3);
    t_1.join();
    t_2.join();
    t_3.join();

此时只会唤醒一个线程,然后一直等待,并且唤醒的线程是随机的。
C++多线程之旅-条件变量condition variables_第4张图片
C++多线程之旅-条件变量condition variables_第5张图片

如果把notify_one换成notify_all()那么这个执行的结果就会不一致了。看一下下面这个例子。

mutex mut;
condition_variable condition;
void fun_1(){
    unique_lock<mutex> lock(mut);
    condition.wait(lock);
    cout << "线程B执行开始    " << this_thread::get_id() << endl;
    cout << "线程B执行结束    " << this_thread::get_id() << endl;
}

void fun_2(){
    cout << "线程A执行开始" << endl;
    this_thread::sleep_for(chrono::seconds (1));
    cout << "线程A执行结束" << endl;
    condition.notify_all();
}

void fun_3(){
    unique_lock<mutex> lock(mut);
    condition.wait(lock);
    cout << "线程C执行开始    " << this_thread::get_id() << endl;
    cout << "线程C执行结束    " << this_thread::get_id() << endl;
}

此时两个线程都会被唤醒,但是唤醒的顺序是随机的。
C++多线程之旅-条件变量condition variables_第6张图片

wait_for()&wait_until()

wait_until()使用的频率不高,所以我们这里只介绍wait_for()。至少需要传入两个参数,一个锁一个时间。看看下面这个例子。

mutex mut;
condition_variable condition;
void fun_1(){
    unique_lock<mutex> lock(mut);
    condition.wait(lock);
    cout << "线程B执行开始    " << this_thread::get_id() << endl;
    cout << "线程B执行结束    " << this_thread::get_id() << endl;
}

void fun_2(){
    cout << "线程A执行开始" << endl;
    this_thread::sleep_for(chrono::seconds (4));
    cout << "线程A执行结束" << endl;
    condition.notify_all();
}

void fun_3(){
    unique_lock<mutex> lock(mut);
    condition.wait_for(lock,chrono::seconds(2));
    cout << "线程C执行开始    " << this_thread::get_id() << endl;
    cout << "线程C执行结束    " << this_thread::get_id() << endl;
}

执行结果如下:
C++多线程之旅-条件变量condition variables_第7张图片
线程A先开始,需要执行4后才能结束,然后唤醒其他的线程。线程B则需要被条件变量唤醒才能继续运行。线程C则仅等待2秒,2秒后自动结束等待继续执行。这个结果也是如代码那样,线程C先于线程A结束前开始运行。

这里面用了一个chrono类,这是C++11引入的全新的时间类,有兴趣可以去看一下,比之前的ctime好用很多,而且易于理解。

伪唤醒

虚假唤醒的缘故导致if判断在多线程环境下出错,因为它不再判断条件是否满足,继续从wait()方法之后执行。而while还会再次判断条件是否满足,如果不满足就不会执行。

void fun_3(){
    unique_lock<mutex> lock(mut);
    if (condition.wait_for(lock,chrono::seconds(2),[]{return false;}));
    cout << "线程C执行开始    " << this_thread::get_id() << endl;
    cout << "线程C执行结束    " << this_thread::get_id() << endl;
}

C++多线程之旅-条件变量condition variables_第8张图片
正常来说这个fun_3所在的线程永远不能被唤醒,但是由于这个if的缘故,在测试中会发现确实被唤醒了。这就是伪唤醒
因为if仅仅只能在第一次进入时判断,而被唤醒时不再进行判断,所以此时在条件不成立的时候也会出现被唤醒的情况。可以使用while替代if来避免这个问题。

void fun_3(){
    unique_lock<mutex> lock(mut);
    while (condition.wait_for(lock,chrono::seconds(2),[]{return true;}));
    cout << "线程C执行开始    " << this_thread::get_id() << endl;
    cout << "线程C执行结束    " << this_thread::get_id() << endl;
}

此时就线程C就会一直阻塞,
C++多线程之旅-条件变量condition variables_第9张图片
但是为什么线程B也一直被阻塞呢? 这是因为线程C一直在尝试while循环尝试解锁,所以一直占用则mut互斥元导致线程B无法获得互斥元结束等待。所以在这个里面我们需要注意这个问题,但是while也不是一个很好的方法,因为还是回到了最开始的那个问题,while会占用空间,占用CPU资源。

总结

条件变量的使用使得线程之间异步通信有了一个基础,可以各自线程单独运行,最终合成结果。这样就可以最大化利用CPU资源,但是这样也会出现很多安全方面的问题。这就需要大量的实践经验来积累。下一部分将去看看,如何将条件变量包装得更加好看,更加好用!

你可能感兴趣的:(C++多线程,多线程,c++)