你的浏览器版本过低,可能导致网站不能正常访问!
为了你能正常使用网站功能,请使用这些浏览器。

条件变量简析

[复制链接]
gaosmile 发布时间:2020-8-28 21:53
想必大家开发过程中都会用到多线程,用到多线程基本上都会用到条件变量,你理解的条件变量只是简单的wait和notify吗,最近工作中看同事也都只是简单的使用wait和notify,导致项目出现bug却不知如何fix bug,其实这里面还是有一些坑的,程序喵这里总结给大家。
本文内容简介:



  • 什么是条件变量?
  • 条件变量如何使用?
  • 如何解决条件变量的信号丢失问题?
  • 如何解决条件变量的虚假唤醒问题?

  • 条件变量为什么一定要和锁配合使用?




1

什么是条件变量?

条件变量是多线程程序中用来实现等待和唤醒逻辑常用的方法。通常有wait和notify两个动作,wait用于阻塞挂起线程A,直到另一个线程B通过通过notify唤醒线程A,唤醒后线程A会继续运行。
条件变量在多线程中很常用,在有名的生产者和消费者问题中,消费者如何知道生成者是否生产出了可以消费的产品,通过while循环不停的去判断是否有可消费的产品?众所周知,死循环极其消耗CPU性能,所以需要使用条件变量来阻塞线程,降低CPU占用率。

2

条件变量的使用
拿生产者和消费者问题举例,看下面这段代码:

  1. std::mutex mutex;
  2. std::condition_variable cv;
  3. std::vector<int> vec;

  4. void Consume() {
  5.   std::unique_lock<std::mutex> lock(mutex);
  6.   cv.wait(lock);
  7.   std::cout << "consume " << vec.size() << "\n";
  8. }

  9. void Produce() {
  10.   std::unique_lock<std::mutex> lock(mutex);
  11.   vec.push_back(1);
  12.   cv.notify_all();
  13.   std::cout << "produce \n";
  14. }

  15. int main() {
  16.   std::thread t(Consume);
  17.   t.detach();
  18.   Produce();
  19.   return 0;
  20. }
复制代码
本意是消费者线程阻塞,等待生产者生产数据后去通知消费者线程,这样消费者线程就可以拿到数据去消费。
但这里有个问题:
如果先执行的Produce(),后执行的Consume(),生产者提前生产出了数据,去通知消费者,但是此时消费者线程如果还没有执行到wait语句,即线程还没有处于挂起等待状态,线程没有等待此条件变量上,那通知的信号就丢失了,后面Consume()中才执行wait处于等待状态,但此时生产者已经不会再触发notify,那消费者线程就会始终阻塞下去,出现bug。
如何解决这个问题呢?可以附加一个判断条件,就可以解决这种信号丢失问题,见代码:

  1. std::mutex mutex;
  2. std::condition_variable cv;
  3. std::vector<int> vec;

  4. void Consumer() {
  5.   std::unique_lock<std::mutex> lock(mutex);
  6.   if (vec.empty()) { // 加入此判断条件
  7.       cv.wait(lock);
  8.   }
  9.   std::cout << "consumer " << vec.size() << "\n";
  10. }

  11. void Produce() {
  12.   std::unique_lock<std::mutex> lock(mutex);
  13.   vec.push_back(1);
  14.   cv.notify_all();
  15.   std::cout << "produce \n";
  16. }

  17. int main() {
  18.   std::thread t(Consumer);
  19.   t.detach();
  20.   Produce();
  21.   return 0;
  22. }
复制代码
通过增加附加条件可以解决信号丢失的问题,但这里还有个地方需要注意,消费者线程处于wait阻塞状态时,即使没有调用notify,操作系统也会有一些概率会唤醒处于阻塞的线程,使其继续执行下去,这就是虚假唤醒问题,当出现了虚假唤醒后,消费者线程继续执行,还是没有可以消费的数据,出现了bug。  
那怎么解决虚假唤醒的问题呢,可以在线程由阻塞状态被唤醒后继续判断附加条件,看是否满足唤醒的条件,如果满足则继续执行,如果不满足,则继续去等待,体现在代码中,即将if判断改为while循环判断,见代码:

  1. std::mutex mutex;
  2. std::condition_variable cv;
  3. std::vector<int> vec;

  4. void Consumer() {
  5.   std::unique_lock<std::mutex> lock(mutex);
  6.   while (vec.empty()) { // 将if改为while
  7.       cv.wait(lock);
  8.   }
  9.   std::cout << "consumer " << vec.size() << "\n";
  10. }

  11. void Produce() {
  12.   std::unique_lock<std::mutex> lock(mutex);
  13.   vec.push_back(1);
  14.   cv.notify_all();
  15.   std::cout << "produce \n";
  16. }

  17. int main() {
  18.   std::thread t(Consumer);
  19.   t.detach();
  20.   Produce();
  21.   return 0;
  22. }
复制代码
看到这里相信你已经明白条件变量的使用啦,需要使用while循环附加判断条件来解决条件变量的信号丢失和虚假唤醒问题。


3

有没有更简单的“避坑”方式

难道我们每次都必须要使用while循环和附加条件来操作条件变量吗?这岂不是很麻烦?
NO!

在C++中其实有更好的封装,只需要调用wait函数时,在参数中直接添加附加条件就好了,内部已经做好了while循环判断,直接使用即可,见代码:

  1. std::mutex mutex;
  2. std::condition_variable cv;
  3. std::vector<int> vec;

  4. void Consumer() {
  5.   std::unique_lock<std::mutex> lock(mutex);
  6.   cv.wait(lock, [&](){ return !vec.empty(); }); // 这里可以直接使用C++的封装
  7.   std::cout << "consumer " << vec.size() << "\n";
  8. }

  9. void Produce() {
  10.   std::unique_lock<std::mutex> lock(mutex);
  11.   vec.push_back(1);
  12.   cv.notify_all();
  13.   std::cout << "produce \n";
  14. }

  15. int main() {
  16.   std::thread t(Consumer);
  17.   t.detach();
  18.   Produce();
  19.   return 0;
  20. }
复制代码
但在C语言中就没办法啦,大家只能自己做一层封装啦。


4

为什么条件变量需要和锁配合使用?

为什么叫条件变量呢?
因为内部是通过判断及修改某个全局变量来决定线程的阻塞与唤醒,多线程操作同一个变量肯定需要加锁来使得线程安全。同时,一个简单的wait函数调用内部会很复杂的,有可能线程A调用了wait函数但是还没有进入到wait阻塞等待前,另一个线程B在此时却调用了notify函数,此时nofity的信号就丢失啦,如果加了锁,线程B必须等待线程A释放了锁并进入了等待状态后才可以调用notify,继而防止信号丢失。
关于条件变量就介绍到这里,希望大家能有所收获,平时使用过程中可以避掉条件变量的坑。


收藏 评论0 发布时间:2020-8-28 21:53

举报

0个回答

所属标签

STM32团队

意法半导体微控制器和微处理器拥有广泛的产品线,包含低成本的8位单片机和基于ARM® Cortex®-M0、M0+、M3、M4、M33、M7及A7内核并具备丰富外设选择的32位微控制器及微处理器


最新内容

关于
我们是谁
投资者关系
意法半导体可持续发展举措
创新与技术
意法半导体官网
联系我们
联系ST分支机构
寻找销售人员和分销渠道
社区
媒体中心
活动与培训
隐私策略
隐私策略
Cookies管理
行使您的权利
官方最新发布
STM32N6 AI生态系统
STM32MCU,MPU高性能GUI
ST ACEPACK电源模块
意法半导体生物传感器
STM32Cube扩展软件包
关注我们
微信公众号二维码 微信公众号
手机版二维码 手机版