C++11线程库 (六) 条件变量 Condition variables

条件变量是一个线程同步原语,它允许多个线程相互通信。
他可以让一部分线程等待(可能超时)另一个线程可能发出的继续执行通知,条件变量与总是和互斥锁相关。————cppref对于条件变量的描述

一、为什么需要条件变量?

简单来说,条件变量可以完成一些特定场合互斥锁不能(高效)完成的工作。类似于一个线程阻塞至另一线程满足某一要求时才开始执行。如果用互斥锁实现这样的一个过程,

二、C++对Condition variables相关支持

#include <condition_variable>

  • condition_variable(C++11) 提供一个unique_lock类型的条件变量(类)
  • condition_variable_any(C++11) 提供一个任意锁类型的条件变量(类)
  • notify_all_at_thread_exit(C++11) 计划一个在线程完全退出时对notify_all的调用(函数)
  • cv_status(C++11) 在条件变量中使用定时等待时,枚举所有可能(time_out or no_timeout)结果(枚举类型)

三、条件变量 condition_variable

3.1 简介

条件变量是一个同步原语,在同一时刻可用于阻塞一个或者多个线程,知道其他线程修改共享条件,并通知条件变量。

假若一个线程尝试修改共享变量,他需要:

  • 获得std::mutex(最典型的就是通过std::lock_gurad)
  • 获得lock的时候进行修改
  • 在条件变量上调用notify_one或者notify_all(此时这个锁不需要先获得)

请注意,即使共享变量是一个原子型的,其仍需要在mutex下进行修改一遍正确发布到等待线程中。???

任何有意在 std::condition_variable 上等待的线程必须:

  • 在与用于保护共享变量者相同的互斥上获得 std::unique_lockstd::mutex
    执行下列之一:
  • 检查条件,是否为已更新或提醒它的情况
  • 执行 wait 、 wait_for 或 wait_until ,等待操作自动释放互斥,并悬挂线程的执行。
  • condition_variable 被通知时,时限消失或虚假唤醒发生,线程被唤醒,且自动重获得互斥。之后线程应检查条件,若唤醒是虚假的,则继续等待。如果你不想这么麻烦,你可以使用 waitwait_forwait_until函数的谓词重载形式,它能够帮你完成上述三件事情,简化程序书写。

!!std::condition_variable 只可与 std::unique_lockstd::mutex 一同使用;此限制能让这种机制在一些平台取得最高的效率。当然如果你想使用与锁类型无关的条件变量,可以试试他的兄弟::std::condition_variable_any

3.2 成员函数

构造函数只有无参默认构造函数,无拷贝、无赋值。剩下的就是两类成员函数,一是通知方法,二是等待方法。

通知方法:要么通知所有人notify_one,要么通知一个人notify_all

等待方法:等待wait、等待一段时间wait_for和等待某个时刻wait_until。使用的方式就是“条件变量调用某种方法等待unique_lcok”

cond.wait(ulo);

调用wait方法导致当前线程阻塞直至条件变量被通知,或虚假唤醒发生,可选地循环直至满足某谓词。为了避免虚假唤醒,你可以使用谓词:

template< class Predicate >
void wait( std::unique_lock<std::mutex>& lock, Predicate pred );

等价于以下语句:

while (!pred()) {
    
    //不满足继续等待
    wait(lock);
}

这个谓词应该理解为接触阻塞条件。

三、条件变量实例

这个例子实现了按序打印并发线程:

#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
using namespace std;

class ConditionVarTest
{
    
    
public:
    void printone();
    void printtwo();
    void printthree();
    mutex lo;
    condition_variable cond;
    int i=0;
};

void ConditionVarTest::printone(){
    
    
    cout<<"1";
    i=1;
    cond.notify_all();
}
 
void ConditionVarTest::printtwo(){
    
    
    unique_lock<mutex> ulo(lo);
    cond.wait(ulo,[this]{
    
    return i==1;});
    cout<<"2";
    i=2;
    cond.notify_one();
}

void ConditionVarTest::printthree(){
    
    
    unique_lock<mutex> ulo(lo);
    cond.wait(ulo,[this]{
    
    return i==2;});
    cout<<"3";

}

int main()
{
    
    
    ConditionVarTest a;
    thread th1(&ConditionVarTest::printone,&a);
    thread th2(&ConditionVarTest::printtwo,&a);
    thread th3(&ConditionVarTest::printthree,&a);
    
    
    th1.join();
    th2.join();
    th3.join();
    
    return 0;
    
}

四、虚假唤醒

条件变量还应该注意的地方在于虚假唤醒,这常常出现在生产-消费设计模式中:

  • 当消费者的数量(CONSUMER_NUM)为1时没有其他线程竞争队列,不会触发虚假唤醒。
  • 当生产者的数量(PRODUCTER_NUM)为1时,
    当使用notify_one通知消费线程时,不会发生虚假唤醒,因为每次只会有一个消费者线程收到信号被唤醒,在产品被消耗掉之前不会有新的信号发出来。
    当使用notify_all通知消费线程时,会发生虚假唤醒,会有多个消费者线程收到信号被唤醒,当一个线程被唤醒之前,可能其他线程先被唤醒先持有锁,将产品消耗掉。
  • 当生产者的数量(PRODUCTER_NUM)大于1时,无论是使用notify_one,或者是notify_all都会发生虚假唤醒,当多个生产者使用notify_one时,多个线程被唤醒,有可能其中一个处理的特别快,将所有的数据都处理完毕,那么接下来被唤醒的线程都无数据可处理

处理虚假唤醒的做法,消费者唤醒后使用while反复对产品数进行检查。为什么使用while?在单核下,使用broadcast()会导致惊群效应进而导致虚假唤醒,但使用notify()显然不会出现虚假唤醒(只会唤醒某一个线程)。但在多核处理器下,pthread_cond_signal可能会激活多于一个阻塞在条件变量上的线程(多核情况下:当notify时有多个线程同时被唤醒)。结果就是,当一个线程调用pthread_cond_signal()后,多个调用pthread_cond_wait()或pthread_cond_timedwait()的线程返回。这种效应就称为“惊群效应”。如果用if判断,多个等待线程在满足if条件时都会被唤醒(虚假的),但实际上条件并不满足,生产者生产出来的消费品已经被第一个线程消费了,导致“虚假唤醒”。因此需要使用while以对条件进行再判断以避免虚假唤醒。

[1] https://www.jianshu.com/p/01ad36b91d39
[2] https://blog.csdn.net/shizheng163/article/details/83661861

猜你喜欢

转载自blog.csdn.net/weixin_39258979/article/details/114199315