C++ 条件变量使用详解

举报
C语言与CPP编程 发表于 2022/08/25 00:22:20 2022/08/25
【摘要】 【导读】:本文主要讲解条件变量的详细使用方法。 condition_variable介绍 在C++11中,我们可以使用条件变量(condition_variable)实现多个线程间的同步操作;当条件不满足时,相关线程被一直阻塞,直到某种条件出现,这些线程才会被唤醒。 其主要成员函数如下: 条件变量是利用线程间共享的全局变...

【导读】:本文主要讲解条件变量的详细使用方法。

condition_variable介绍

在C++11中,我们可以使用条件变量(condition_variable)实现多个线程间的同步操作;当条件不满足时,相关线程被一直阻塞,直到某种条件出现,这些线程才会被唤醒。

其主要成员函数如下:

296865f0286618e48d1939f5a75c6a5b.png

条件变量是利用线程间共享的全局变量进行同步的一种机制,主要包括两个动作:

  • 一个线程因等待"条件变量的条件成立"而挂起;

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

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

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

  • 等待条件成立使用的是condition_variable类成员wait 、wait_for 或 wait_until。

  • 给出信号使用的是condition_variable类成员notify_one或者notify_all函数。

细节说明

在条件变量中只能使用std::unique_lock< std::mutex >说明

unique_lock和lock_guard都是管理锁的辅助类工具,都是RAII风格;它们是在定义时获得锁,在析构时释放锁。它们的主要区别在于unique_lock锁机制更加灵活,可以再需要的时候进行lock或者unlock调用,不非得是析构或者构造时。它们的区别可以通过成员函数就可以一目了然。在这里插入图片描述

wait/wait_for说明

线程的阻塞是通过成员函数wait()/wait_for()/wait_until()函数实现的。这里主要说明前面两个函数:

wait()成员函数

函数声明如下:


   
  1. void wait( std::unique_lock<std::mutex>& lock );
  2. //Predicate 谓词函数,可以普通函数或者lambda表达式
  3. template< class Predicate >
  4. void wait( std::unique_lock<std::mutex>& lock, Predicate pred );

wait 导致当前线程阻塞直至条件变量被通知,或虚假唤醒发生,可选地循环直至满足某谓词。

wait_for()成员函数

函数声明如下:


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

   
  1. template< class Rep, class Period, class Predicate >
  2. bool wait_for( std::unique_lock<std::mutex>& lock,
  3.                const std::chrono::duration<Rep, Period>& rel_time,
  4.                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一起使用的原因,否则当前线程一直占有锁,线程被阻塞。

notify_all/notify_one

notify函数声明如下:

void notify_one() noexcept;
  

若任何线程在 *this 上等待,则调用 notify_one 会解阻塞(唤醒)等待线程之一。

void notify_all() noexcept;
  

若任何线程在 *this 上等待,则解阻塞(唤醒)全部等待线程。

虚假唤醒

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


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

原因说明如下:

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


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

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


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

条件变量使用

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

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

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

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

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

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


   
  1. std::mutex g_cvMutex;
  2. std::condition_variable g_cv;
  3. //缓存区
  4. std::deque<int> g_data_deque;
  5. //缓存区最大数目
  6. const int  MAX_NUM = 30;
  7. //数据
  8. int g_next_index = 0;
  9. //生产者,消费者线程个数
  10. const int PRODUCER_THREAD_NUM  = 3;
  11. const int CONSUMER_THREAD_NUM = 3;
  12. void  producer_thread(int thread_id)
  13. {
  14.   while (true)
  15.   {
  16.       std::this_thread::sleep_for(std::chrono::milliseconds(500));
  17.       //加锁
  18.       std::unique_lock <std::mutex> lk(g_cvMutex);
  19.       //当队列未满时,继续添加数据
  20.       g_cv.wait(lk, [](){ return g_data_deque.size() <= MAX_NUM; });
  21.       g_next_index++;
  22.       g_data_deque.push_back(g_next_index);
  23.       std::cout << "producer_thread: " << thread_id << " producer data: " << g_next_index;
  24.       std::cout << " queue size: " << g_data_deque.size() << std::endl;
  25.       //唤醒其他线程 
  26.       g_cv.notify_all();
  27.       //自动释放锁
  28.   }
  29. }
  30. void  consumer_thread(int thread_id)
  31. {
  32.     while (true)
  33.     {
  34.         std::this_thread::sleep_for(std::chrono::milliseconds(550));
  35.         //加锁
  36.         std::unique_lock <std::mutex> lk(g_cvMutex);
  37.         //检测条件是否达成
  38.         g_cv.wait( lk,   []{ return !g_data_deque.empty(); });
  39.         //互斥操作,消息数据
  40.         int data = g_data_deque.front();
  41.         g_data_deque.pop_front();
  42.         std::cout << "\tconsumer_thread: " << thread_id << " consumer data: ";
  43.         std::cout << data << " deque size: " << g_data_deque.size() << std::endl;
  44.         //唤醒其他线程
  45.         g_cv.notify_all();
  46.         //自动释放锁
  47.     }
  48. }
  49. int main()
  50. {
  51.     std::thread arrRroducerThread[PRODUCER_THREAD_NUM];
  52.     std::thread arrConsumerThread[CONSUMER_THREAD_NUM];
  53.     for (int i = 0; i < PRODUCER_THREAD_NUM; i++)
  54.     {
  55.         arrRroducerThread[i] = std::thread(producer_thread, i);
  56.     }
  57.     for (int i = 0; i < CONSUMER_THREAD_NUM; i++)
  58.     {
  59.         arrConsumerThread[i] = std::thread(consumer_thread, i);
  60.     }
  61.     for (int i = 0; i < PRODUCER_THREAD_NUM; i++)
  62.     {
  63.         arrRroducerThread[i].join();
  64.     }
  65.     for (int i = 0; i < CONSUMER_THREAD_NUM; i++)
  66.     {
  67.         arrConsumerThread[i].join();
  68.     }
  69.     
  70.  return 0;
  71. }

运行结果:d24dbf7fc2a0690206200a1f7aa80dcd.png

- EOF -

点赞和在看就是最大的支持❤️

文章来源: blog.csdn.net,作者:程序员编程指南,版权归原作者所有,如需转载,请联系作者。

原文链接:blog.csdn.net/weixin_41055260/article/details/126495924

【版权声明】本文为华为云社区用户转载文章,如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。