[多线程]C++11多线程-条件变量(std::condition_variable)

Posted ouyangshima

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[多线程]C++11多线程-条件变量(std::condition_variable)相关的知识,希望对你有一定的参考价值。

互斥量(std::mutex)是多线程间同时访问某一共享变量时,保证变量可被安全访问的手段。在多线程编程中,还有另一种十分常见的行为:线程同步线程同步是指线程间需要按照预定的先后次序顺序进行的行为。C++11对这种行为也提供了有力的支持,这就是条件变量。条件变量位于头文件condition_variable。条件变量能阻塞一个或多个线程,直到收到另外一个线程发出的通知或者超时,才会唤醒当前阻塞的线程。条件变量需要和互斥量配合起来用。

  • condition_variable,配合std::unique_lock < std::mutex>进行wait操作。
  • condition_variable_any,和任意带有lock、unlock语义的mutex搭配使用,比较灵活,但效率比condition_variable差一些。

condition_variable是一个类,搭配互斥量mutex来用,这个类有它自己的一些函数,这里就主要讲wait函数和notify_*函数,故名思意,wait就是有一个等待的作用,notify就是有一个通知的作用简而言之就是程序运行到wait函数的时候会先在此阻塞,然后自动unlock,那么其他线程在拿到锁以后就会往下运行,当运行到notify_one()函数的时候,就会唤醒wait函数,然后自动lock并继续下运行。

wait

wait是线程的等待动作,直到其它线程将其唤醒后,才会继续往下执行。

std::mutex mutex;
std::condition_variable cv;

// 条件变量与临界区有关,用来获取和释放一个锁,因此通常会和mutex联用。
std::unique_lock lock(mutex);
// 此处会释放lock,然后在cv上等待,直到其它线程通过cv.notify_xxx来唤醒当前线程,
// cv被唤醒后会再次对lock进行上锁,然后wait函数才会返回。
// wait返回后可以安全的使用mutex保护的临界区内的数据。此时mutex仍为上锁状态
cv.wait(lock)

需要注意的一点是, wait有时会在没有任何线程调用notify的情况下返回,这种情况就是有名的虚假唤醒。因此当wait返回时,你需要再次检查wait的前置条件是否满足,如果不满足则需要再次wait。wait提供了重载的版本,用于提供前置检查。

wait还有第二个参数,这个参数接收一个布尔类型的值,当这个布尔类型的值为false的时候线程就会被阻塞在这里,只有当该线程被唤醒之后,且第二参数为true才会往下运行。

template <typename Predicate>
void wait(unique_lock<mutex> &lock, Predicate pred) 
    while(!pred()) 
        wait(lock);
    

除wait外, 条件变量还提供了wait_for和wait_until,这两个名称是不是看着有点儿眼熟,std::mutex也提供了_for和_until操作。在C++11多线程编程中,需要等待一段时间的操作,一般情况下都会有xxx_for和xxx_until版本。前者用于等待指定时长,后者用于等待到指定的时间。

notify

了解了wait,notify就简单多了:唤醒wait在该条件变量上的线程。notify有两个版本:notify_one和notify_all。

  • notify_one 唤醒等待的一个线程,注意只唤醒一个。
  • notify_all 唤醒所有等待的线程。使用该函数时应避免出现惊群效应(多个线程等待一个唤醒的情况叫做惊群效应)。
std::mutex mutex;
std::condition_variable cv;

std::unique_lock lock(mutex);
// 所有等待在cv变量上的线程都会被唤醒。但直到lock释放了mutex,被唤醒的线程才会从wait返回。
cv.notify_all(lock)

notify_one()每次只能唤醒一个线程,那么notify_all()函数的作用就是可以唤醒所有的线程但是最终能抢夺锁的只有一个线程,或者说有多个线程在wait,但是用notify_one()去唤醒其中一个线程,那么这些线程就出现了去争夺互斥量的一个情况,那么最终没有获得锁的控制权的线程就会再次回到阻塞的状态,那么对于这些没有抢到控制权的这个过程就叫做虚假唤醒那么对于虚假唤醒的解决方法就是加一个while循环,比如下面这样:

while (que.size() == 0) 
    cr.wait(lck);

这个就是当线程被唤醒以后,先进行判断,是否可以去操作,如果可以再去运行下面的代码,否则继续在循环内执行wait函数。

条件变量使用

在这里,我们使用条件变量,解决生产者-消费者问题,该问题主要描述如下:
生产者-消费者问题,也称有限缓冲问题,是一个多进程/线程同步问题的经典案例。该问题描述了共享固定大小缓冲区的两个进程/线程——即所谓的“生产者”和“消费者”,在实际运行时会发生的问题。
生产者的主要作用是生成一定量的数据放到缓冲区中,然后重复此过程。与此同时,消费者也在缓冲区消耗这些数据。该问题的关键就是要保证生产者不会在缓冲区满时加入数据,消费者也不会在缓冲区中空时消耗数据
要解决该问题,就必须让生产者在缓冲区满时休眠(要么干脆就放弃数据),等到下次消费者消耗缓冲区中的数据的时候,生产者才能被唤醒,开始往缓冲区添加数据。同样,也可以让消费者在缓冲区空时进入休眠,等到生产者往缓冲区添加数据之后,再唤醒消费者。

生产者-消费者代码如下:

#include <iostream>
#include <thread>
#include <mutex>
#include <queue>
#include <windows.h>
#include <condition_variable>

std::mutex g_cvMutex;// 全局互斥锁
std::condition_variable g_cv; // 全局条件变量

std::deque<int> g_data_deque;//缓冲区,全局消息队列
const int  MAX_NUM = 30;//缓存区最大数目
int g_next_index = 0;//数据

//生产者,消费者线程个数
const int PRODUCER_THREAD_NUM  = 3;
const int CONSUMER_THREAD_NUM = 3;

void producer_thread(int thread_id)

    while (true)
    
        std::this_thread::sleep_for(std::chrono::milliseconds(500));
        std::unique_lock <std::mutex> lock(g_cvMutex);//加锁
        //当队列未满时,继续添加数据
        g_cv.wait(lock, []() return g_data_deque.size() <= MAX_NUM; );
        g_next_index++;
        g_data_deque.push_back(g_next_index);
        std::cout << "producer_thread: " << thread_id << " producer data: " << g_next_index;
        std::cout << " queue size: " << g_data_deque.size() << std::endl;

        // 通知前,手动解锁以防正在等待的线程被唤醒后又立即被阻塞。
        lock.unlock();
        g_cv.notify_all();//唤醒其他线程
    


void consumer_thread(int thread_id)

    while (true)
    
        
            std::this_thread::sleep_for(std::chrono::milliseconds(550));
            std::unique_lock<std::mutex> lock(g_cvMutex);//加锁
            g_cv.wait(lock, []  return !g_data_deque.empty(); );//检测条件是否达成

            //互斥操作,消息数据
            int data = g_data_deque.front();
            g_data_deque.pop_front();
            std::cout << "\\tconsumer_thread: " << thread_id << " consumer data: ";
            std::cout << data << " deque size: " << g_data_deque.size() << std::endl;

            // 这里用大括号括起来了 为了避免出现虚假唤醒的情况 所以先unlock 再去唤醒
        
        g_cv.notify_all(); //唤醒其他线程
    


int main()

    std::thread arrRroducerThread[PRODUCER_THREAD_NUM];
    std::thread arrConsumerThread[CONSUMER_THREAD_NUM];

    for (int i = 0; i < PRODUCER_THREAD_NUM; i++)
    
        arrRroducerThread[i] = std::thread(producer_thread, i);
    

    for (int i = 0; i < CONSUMER_THREAD_NUM; i++)
    
        arrConsumerThread[i] = std::thread(consumer_thread, i);
    

    for (int i = 0; i < PRODUCER_THREAD_NUM; i++)
    
        arrRroducerThread[i].join();
    

    for (int i = 0; i < CONSUMER_THREAD_NUM; i++)
    
        arrConsumerThread[i].join();
    
    return 0;

/*输出
producer_thread: 2 producer data: 1 queue size: 1
producer_thread: 0 producer data: 2 queue size: 2
producer_thread: 1 producer data: 3 queue size: 3
        consumer_thread: 0 consumer data: 1 deque size: 2
        consumer_thread: 1 consumer data: 2 deque size: 1
        consumer_thread: 2 consumer data: 3 deque size: 0
producer_thread: 2 producer data: 4 queue size: 1
...
*/

 

以上是关于[多线程]C++11多线程-条件变量(std::condition_variable)的主要内容,如果未能解决你的问题,请参考以下文章

[C++11 多线程同步] --- 条件变量

[C++11 多线程同步] --- 条件变量

C++11多线程 条件变量condition_variable

C++11多线程编程——生产消费者模型之条件变量

[C++11 多线程同步] --- 条件变量的那些坑条件变量信号丢失和条件变量虚假唤醒(spurious wakeup)

[C++11 多线程同步] --- 条件变量的那些坑条件变量信号丢失和条件变量虚假唤醒(spurious wakeup)