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

条件变量简析

[复制链接]
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扩展软件包
关注我们
st-img 微信公众号
st-img 手机版