条件变量condition_variable实现线程同步

7 篇文章 0 订阅

一.condition_variable使用

1.1 condition_variable条件变量介绍

  在C++11中,我们可以使用条件变量(condition_variable)实现多个线程间的同步操作;当条件不满足时,相关线程被一直阻塞,直到某种条件出现,这些线程才会被唤醒。
  其相关的成员函数如下:
在这里插入图片描述
条件变量是利用线程间共享的全局变量进行同步的一种机制,主要包括两个动作:

  • 一个线程因等待"条件变量的条件成立"而挂起;
  • 另外一个线程使"条件成立",给出信号,从而唤醒被等待的线程。

  为了防止竞争,条件变量的使用总是和一个互斥锁结合在一起;通常情况下这个锁是std::mutex,并且管理这个锁 只能是 std::unique_lock<std::mutex> RAII模板类。

上面提到的两个步骤,分别是使用以下两个方法实现:

  • 等待条件成立使用的是condition_variable类成员wait 、wait_for 或 wait_until。
  • 给出信号使用的是condition_variable类成员notify_one或者notify_all函数。

1.2 condition_variable成员函数使用

1.2.1 wait()成员函数

(1)void wait( std::unique_lock<std::mutex>& lock )
  先unlock释放之前获得的mutex,然后阻塞当前的执行线程。把当前线程添加到等待线程列表中,该线程会持续 block 直到被 notify_all() 或 notify_one() 唤醒。被唤醒后,该thread会重新获取mutex,获取到mutex后执行后面的动作

示例:

#include <iostream>        
#include <condition_variable>
#include <mutex>
#include <thread>

std::mutex               g_mtx_;
std::condition_variable  g_cond_;

using namespace std;

void test_wait() {
    //定义一个unique_lock互斥锁变量lock
    std::unique_lock<std::mutex> lock(g_mtx_);
    //条件变量阻塞等待
    g_cond_.wait(lock);

    printf("---------I am test_wait---------\n");
}

int main() {
    printf("-------------test_wait-------------\n");
    std::thread t1(test_wait);    //创建子线程t1
    printf("------------sleep start------------\n");
    printf("这里延时了2s\n");
    std::this_thread::sleep_for(2000ms);  //主线程阻塞2s,此时子线程在阻塞wait
    printf("-------------sleep end-------------\n");
    {
        //在发送通知给到子线程条件变量之前,需要给mutex互斥锁上锁
        std::unique_lock<std::mutex> lock(g_mtx_);
    }
    //通知一个子线程
    g_cond_.notify_one();
    //回收子线程t1
    t1.join();
    return 0;
}

编译 && 运行:

g++ mian.cpp -lpthread
./a.out

执行结果:
在这里插入图片描述
(2)template< class Predicate > void wait( std::unique_lock<std::mutex>& lock, Predicate pred ); //Predicate 谓词函数,可以普通函数或者lambda表达式

  wait 导致当前线程阻塞直至条件变量被通知,或虚假唤醒发生,可选地循环直至满足某谓词。该重载设置了第二个参数 Predicate, 只有当pred为false时,wait才会阻塞当前线程。等同于下面:

template<typename _Predicate>
void wait(unique_lock<mutex>& __lock, _Predicate __pred)
{
    while (!__pred())
        wait(__lock);
}

  该情况下,线程被唤醒后,先重新判断pred的值如果pred为false,则会释放mutex并重新阻塞在wait。因此,该mutex必须有pred的权限。该重载消除了意外唤醒的影响。总的来说,如果想要唤醒条件变量的wait等待,首先,需要_Predicate lambda表达式执行为true;然后,需要获得notify

示例:

#include <iostream>        
#include <atomic>
#include <condition_variable>
#include <future>
#include <mutex>
#include <thread>

std::mutex               g_mtx_;     //定义互斥信号量
std::condition_variable  g_cond_;    //定义条件变量
std::atomic<bool>        g_pred_;    //定义原子变量

using namespace std;

void test_wait_pred() {
    //定义互斥信号量lock
    std::unique_lock<std::mutex> lock(g_mtx_);
    //条件变量wait等待notify,首先阻塞等待g_pred_.load() == true,然后再wait
    g_cond_.wait(lock, []{ 
        std::cout<<"_Predicate lambda执行!!!"<<std::endl;
        return g_pred_.load(); 
        }); //等同于下面的表达
    /*
        {
        while (!lambda())
            wait(__lock);
        }
    */
    printf("---------I am test_wait_pred---------\n");
}

int main() {
    printf("----------test_wait_pred-----------\n");
    std::thread t1(test_wait_pred);  //创建一个子线程t1
    printf("----------awaken first-------------\n");
    {
        //首先给mutex互斥信号量上锁
        std::unique_lock<std::mutex> lock(g_mtx_);
    }
    //给子线程的条件变量g_cond_发通知,此时并不会触发子线程推出wait,因为__pred == false
    //所以第一次唤醒会失败!
    g_cond_.notify_one();
    this_thread::sleep_for(2000ms);
    printf("此处延时了2s\n");
    
    printf("----------awaken second------------\n");
    {
        //首先给mutex互斥信号量上锁
        std::unique_lock<std::mutex> lock(g_mtx_);
        //原子变量置为true,激活__pred == true
        g_pred_.store(true);
    }
    //给子线程的条件变量g_cond_发通知
    g_cond_.notify_one();
    //回收子线程
    t1.join();
    return 0;
}

编译 && 运行:

g++ mian.cpp -lpthread
./a.out

执行结果:
在这里插入图片描述
  分析
  这里在主线程中对子线程的cv.wait()尝试唤醒了两次,第一次是直接发送notify_one();而并没有使得_Predicate的lambda表法式返回true,所以awaken first只是执行了一下_Predicate的lambda表法式,而并没有唤醒子线程而使得子线程推出阻塞;
  第二次是先激活__pred == true,使得_Predicate的lambda表法式返回为true,然后再发送notify_one();所以第二次成功的唤醒了子线程。
  另外,可以看出,每次由主线程发送的notify_one(),都会使得_Predicate的lambda表法式执行一次,不同的是要根据lambda表法式的输出结果来决定要不要使得cv.wait()唤醒!

1.2.2 wait_for()成员函数

函数声明如下:

1template< class Rep, class Period >
std::cv_status wait_for( std::unique_lock<std::mutex>& lock,
                         const std::chrono::duration<Rep, Period>& rel_time);2template< class Rep, class Period, class Predicate >
bool wait_for( std::unique_lock<std::mutex>& lock,
               const std::chrono::duration<Rep, Period>& rel_time,
               Predicate pred);

wait_for 导致当前线程阻塞直至条件变量被通知,或虚假唤醒发生,或者超时返回。

返回值说明:

(1)若经过 rel_time 所指定的关联时限则为 std::cv_status::timeout ,否则为 std::cv_status::no_timeout 。

(2)若经过 rel_time 时限后谓词 pred 仍求值为 false 则为 false ,否则为 true 。

以上两个类型的wait函数都在会阻塞时,自动释放锁权限,即调用unique_lock的成员函数unlock(),以便其他线程能有机会获得锁。这就是条件变量只能和unique_lock一起使用的原因,否则当前线程一直占有锁,线程被阻塞。

1.2.3 notify_all/notify_one成员函数

notify函数声明如下:

//若任何线程在 *this 上等待,则调用 notify_one 会解阻塞(唤醒)等待线程之一。
void notify_one() noexcept;
//若任何线程在 *this 上等待,则解阻塞(唤醒)全部等待线程。
void notify_all() noexcept;

虚假唤醒:
  在正常情况下,wait类型函数返回时要不是因为被唤醒,要不是因为超时才返回,但是在实际中发现,因此操作系统的原因,wait类型在不满足条件时,它也会返回,这就导致了虚假唤醒。因此,我们一般都是使用带有谓词参数的wait函数,因为这种(xxx, Predicate pred )类型的函数等价于:

while (!pred()) //while循环,解决了虚假唤醒的问题
{
    wait(lock);
}

原因说明如下:

假设系统不存在虚假唤醒的时,代码形式如下:

if (不满足xxx条件)
{
    //没有虚假唤醒,wait函数可以一直等待,直到被唤醒或者超时,没有问题。
    //但实际中却存在虚假唤醒,导致假设不成立,wait不会继续等待,跳出if语句,
    //提前执行其他代码,流程异常
    wait();  
}
//其他代码
...

正确的使用方式,使用while语句解决:

while (!(xxx条件) )
{
    //虚假唤醒发生,由于while循环,再次检查条件是否满足,
    //否则继续等待,解决虚假唤醒
    wait();  
}
//其他代码
....

1.3 条件变量的使用场景示例:生产者消费者问题

在这里,我们使用条件变量,解决生产者-消费者问题,该问题主要描述如下:

生产者-消费者问题,也称有限缓冲问题,是一个多进程/线程同步问题的经典案例。该问题描述了共享固定大小缓冲区的两个进程/线程——即所谓的“生产者”和“消费者”,在实际运行时会发生的问题。

生产者的主要作用是生成一定量的数据放到缓冲区中,然后重复此过程。与此同时,消费者在缓冲区消耗这些数据。该问题的关键就是要保证生产者不会在缓冲区满时加入数据,消费者也不会在缓冲区中空时消耗数据。

要解决该问题,就必须让生产者在缓冲区满时休眠(要么干脆就放弃数据),等到下次消费者消耗缓冲区中的数据的时候,生产者才能被唤醒,开始往缓冲区添加数据。

同样,也可以让消费者在缓冲区空时进入休眠,等到生产者往缓冲区添加数据之后,再唤醒消费者。

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

#include <iostream>        
#include <atomic>
#include <condition_variable>
#include <future>
#include <mutex>
#include <thread>
#include <deque>

using namespace std;

std::mutex g_cvMutex;           //定义条件变量使用的mutex
std::condition_variable g_cv;   //定义一个条件变量
//缓存deque区
std::deque<int> g_data_deque;
//缓存区最大数目
const int  MAX_NUM = 30;
//数据
int g_next_index = 0;
//生产者,消费者线程个数:3
const int PRODUCER_THREAD_NUM  = 3;
const int CONSUMER_THREAD_NUM = 3;

//生产者线程
void  producer_thread(int thread_id)
{
	 while (true)
	 {
         //生产者线程休眠500ms,而消费者休眠550ms,所以生产者生产的速度要比消费者快
	     std::this_thread::sleep_for(std::chrono::milliseconds(500));
	     //加锁
	     std::unique_lock <std::mutex> lk(g_cvMutex);
	     //当队列未满时,继续添加数据
	     g_cv.wait(lk, [](){ return g_data_deque.size() <= MAX_NUM; });
	     g_next_index++;  //数据值+1
	     g_data_deque.push_back(g_next_index); //将增加的数据push到deque中
	     std::cout << "producer_thread: " << thread_id << "          producer data: " << g_next_index;
	     std::cout << "         queue size: " << g_data_deque.size() << std::endl;
	     //唤醒其他线程 
	     g_cv.notify_all();
	     //自动释放锁
	 }
}
void  consumer_thread(int thread_id)
{
    while (true)
    {
        //生产者线程休眠500ms,而消费者休眠550ms,所以生产者生产的速度要比消费者快
        std::this_thread::sleep_for(std::chrono::milliseconds(550));
        //加锁
        std::unique_lock <std::mutex> lk(g_cvMutex);
        //检测条件是否达成
        g_cv.wait( lk,   []{ return !g_data_deque.empty(); });
        //互斥操作,消息数据
        int data = g_data_deque.front();  //从deque队列中获取头部数据
        g_data_deque.pop_front();         //deque头部数据出队列
        std::cout << "\tconsumer_thread: " << thread_id << "          consumer data: ";
        std::cout << data << "         deque size: " << g_data_deque.size() << std::endl;
        //唤醒其他线程
        g_cv.notify_all();
        //自动释放锁
    }
}
int main()
{
    std::thread arrRroducerThread[PRODUCER_THREAD_NUM];   //定义生产者线程函数队列(3)
    std::thread arrConsumerThread[CONSUMER_THREAD_NUM];   //定义消费者线程函数队列(3)
    for (int i = 0; i < PRODUCER_THREAD_NUM; i++)
    {
        arrRroducerThread[i] = std::thread(producer_thread, i);  //创建3个生产者线程
    }
    for (int i = 0; i < CONSUMER_THREAD_NUM; i++)
    {
        arrConsumerThread[i] = std::thread(consumer_thread, i);  //创建3个消费者线程
    }
    for (int i = 0; i < PRODUCER_THREAD_NUM; i++)
    {
        arrRroducerThread[i].join();   //回收3个生产者线程
    }
    for (int i = 0; i < CONSUMER_THREAD_NUM; i++)
    {
        arrConsumerThread[i].join();   //回收3个消费者线程
    }
	return 0;
}

编译 && 运行:

g++ mian.cpp -lpthread
./a.out

执行结果:
在这里插入图片描述
  可以看出,生成者和消费者之间通过条件变量g_cv实现了互锁,在生产者线程生产数据的时候,占用了mutex互斥锁,此时消费者线程只能阻塞等待,当生产者完成数据生产之后,会释放mutex互斥锁,并且发送notify通知,告诉消费者可以去deque中取数据了。
  消费者线程获得锁之后,会将mutex上锁以确保生产者线程在消费者读数据的过程中不会向其中写数据。消费者从deque中读取数据,然后通知生产商者线程(线程同步)并释放锁。
  上述的两个线程的cv.wait()都采用了_Predicate lambda的方式,也就是唤醒wait之前必须执行lambda表达式并且返回值为true,上述的lambda表达式是以deque中的数据量作为出发条件,通常可以在两个线程中设置原子变量实现lambda的判断。

二.基于condition_variable条件变量实现线程同步

  下面程序实现了通过子线程获取处理完的数据,整体的思路与上述的生产者-消费者相似,只是将子线程封装在一个类中;在主线程中首先触发_Predicate lambda == true,然后notify通知子线程进行数据处理,并将处理后的数据放在类的buffer中。然后在主线程中wait_async_response(通过原子变量判断)。这里通过类的封装实现请求buffer和响应buffer的预置,在数据请求和数据返回的数据是存储在对象的成员变量中,这样的好处是:在主线程中可以不着急获取数据,而是在需要的时候从子线程对象中获取,实现了主线程buffer和子线程类buffer的双Buffer同步。

程序源码:

#include <iostream>        
#include <atomic>
#include <condition_variable>
#include <future>
#include <mutex>
#include <thread>

using namespace std;

class threadtest
{
public:
    // 外部传入的数据处理函数(可以向远程server完成数据请求)
    using request_func_type = std::function<std::string(const std::string&)>;

    threadtest(request_func_type request_func) 
        : request_done_(true)    //请求结束标志初始设置为true
        , thread_done_(false)    //线程结束标志初始设置为fase
    {
        auto thread_func = [this, request_func] {
            std::cout<<"子线程函数执行"<<std::endl;
            while (wait_async_request()) {
                try {
                    std::cout<<"子线程解锁完成"<<std::endl;
                    auto response = request_func(request_data_);  //调用外部传入的函数对request数据进行处理,获得response数据
                    async_response(std::move(response));          //将response数据进行处理
                }
                catch (std::exception& ex) {
                    std::cout << "[async_response_buffer] request_func exception:" << ex.what() <<std::endl;
                    async_response(std::string());  // 将类的response buffer清空
                }
            }
            std::cout<<"子线程退出执行"<<std::endl;
        };
        //! 开启一个一步处理的子线程,并执行该子线程的函数thread_func
        async_thread_ = std::move(std::thread(thread_func));
    }
    ~threadtest()
    {
        std::cout<<"threadtest析构函数执行,线程结束标志置为true"<<std::endl;
        thread_done_ = true;  //1.线程结束标志置为true,满足_Predicate lambda == true
        {  
            //2.通知子线程结束运行
            std::lock_guard<std::mutex> lg(mutex_);
            cv_.notify_one();
        }
        // 等待子线程执行完成,对子线程进行回收
        if (async_thread_.joinable()) {
            async_thread_.join();
        }
    }

/*
 * 注意:该public下的两个函数均由外部调用(main函数),首先调用async_request发起数据请求;
 * 然后,通过调用wait_async_response函数获取处理好的数据(wait_async_response函数中会等待数据在子线程中处理完成,通过request_done_标志判断)
*/
public:  
    /**
     * @brief  通知子线中的while解除阻塞,开始执行request_func函数对传入的数据进行处理
     * @note   该函数由外部调用,作为启动子线程工作的起点***
     * @param[in]   data  传入的请求数据
     */
    void async_request(string&& data)
    {
        request_done_.store(false); //1.满足_Predicate lambda == true
        {
            // 2.通知子线程的while (wait_async_request())解除阻塞,开始执行
            std::lock_guard<std::mutex> lg(mutex_);
            request_data_ = data;
            cv_.notify_one();   //通知一个等待的线程,notify_all()为通知所有等待的线程
        }
    }
    /**
     * @brief   Wait for async response data store done
     * @note   该函数由外部调用,用来判断数据处理已经完成,并且获得处理后的数据
     * @pre     after @a async_request() called
     * @post    @a response_data_ will be moved
     * @return  Return the async response data @a response_data_
     */
    string&& wait_async_response()
    {
        using namespace std;
        //子线程数据处理过程中request_done_=false,处理结束后request_done_=true,也就跳出while循环,结束该函数
        while (!request_done_.load()) {
            this_thread::sleep_for(10us);
        }
        return std::move(response_data_);
    }

private:
    /**
     * @brief   一直等待直到收到一步通知或线程结束
     * @return  如果线程结束了那么就返回false,使得子线程退出执行
     */
    bool wait_async_request()
    {
        std::unique_lock<std::mutex> ul(mutex_);
        std::cout<<"子线程等待解锁...."<<std::endl;
        //先获得mutex_,然后阻塞当前线程,把当前线程添加到等待线程列表,
        //该线程会持续block直到_Predicate lambda == true并且被 notify_all() 或 notify_one() 唤醒。
        //被唤醒后,该thread会重新获取mutex,获取到mutex后执行后面的动作。
        cv_.wait(ul, [this] { 
            std::cout<<"_Predicate lambda执行,返回值为:"<<(thread_done_ || !request_done_.load())<<std::endl;
            return thread_done_ || !request_done_.load(); 
            });
        return !thread_done_;
    }
    /**
     * @brief   对子线程中获得数据进行处理(存放在类buffer中),处理结束后将request_done_置为true
     * @param[in]   data  子线程处理后的数据
     */
    void async_response(std::string&& data)
    {
        response_data_ = data;
        request_done_.store(true);
    }

private:
    /* 用来通知子线程执行请求(解除锁定) */
    std::mutex              mutex_;
    std::condition_variable cv_;
    /*! 表示子线程中数据的请求、处理过程完成,用来保护request_data_和response_data_ */
    std::atomic<bool> request_done_;
    /* 定义的子线程名和线程执行完成标志 */
    bool        thread_done_;   //线程结束标志,使得子线程退出while循环,子线程执行结束
    std::thread async_thread_;
    /* 用来存放该类请求数据和处理后数据 */
    std::string  request_data_;    
    std::string  response_data_; 
};

// 由主函数传入的数据处理函数
std::string func(const std::string& in_data)
{
    return in_data+"-disposed!";
}
int main(int argc, char** argv)
{
    string request_data1 = "abcdef";
    string request_data2 = "123456";

    string response_data;
    threadtest thread(func);                                 //定义一个threadtest对象为thread,构造参数为func
    
    std::this_thread::sleep_for(2000ms);
    std::cout<<std::endl<<"此处延时了2s"<<std::endl;
    std::cout<<"-------------first request----------------"<<std::endl;
    thread.async_request(std::move(request_data1));           //1.首先发起数据处理请求(子线程解锁)
    response_data = std::move(thread.wait_async_response()); //2.其次,数据处理结束后,获取处理后的数据
    std::cout<<"received data1: "<<response_data<<std::endl;

    std::cout<<"--------------seconed request---------------"<<std::endl;
    thread.async_request(std::move(request_data2));           //1.首先发起数据处理请求(子线程解锁)
    response_data = std::move(thread.wait_async_response()); //2.其次,数据处理结束后,获取处理后的数据
    std::cout<<"received data2: "<<response_data<<std::endl;

    return 0;
}

编译 && 运行:

g++ mian.cpp -lpthread
./a.out

执行结果:
在这里插入图片描述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值