项目场景:

上位机上需要实现一个存文件的操作,本来可以很简单的做实现,想着之前看过一个条件变量的例子,所以胆子肥了写来试试。

大概实现思路如下,有一个数据线程接收数据,通过按钮操作,来置标志位,从而开始写数入队列,入队操作完成后,通过notify函数调用取数线程,取数线程从队列里拿数。然后通过按钮操作置标志位,退出写数线程,好像线程析构这种操作很不稳定,网上大家还是建议利用标志位,也正是因为标志位的问题,导致了函数一直阻塞在wait函数处,线程无法退出,无法实现关闭文件操作,因此无法完成正常写数。


问题描述:

在具体测试中发现会只有程序关闭时,文件才会写入,这是因为file.close()函数未调用实现的原因,而这句函数之所以不被调用就是因为条件变量的wait函数阻塞的问题,标志位置位后,入队操作操作先结束,队列为空,所以代码一直阻塞至wait函数处,导致我设置的全部变量无法通过条件判断来正常退出循环,从而无法正常调用file.close()函数。

TARGET_TXT:while(DataProcessRadar::STORE_READY_FLAG.TARGET_READY_FLAG)//通过标志位判断循环是否结束,该标志位受控件影响
{std::unique_lock<std::mutex> locker(mutex_txt);while(q_local_data_txt.empty()){//if(!DataProcessRadar::STORE_READY_FLAG.TARGET_READY_FLAG)//{//goto TARGET_TXT;//}cond_txt.wait(locker);}StoreData sd = q_local_data_txt.front();if(sd.type == Target)//队列中会存在两种数据类型,因此在此处会做判断,效率有点损失{q_local_data_txt.pop();locker.unlock();}
}
file.close();

背景介绍:

条件变量入坑时,是看到一个互斥锁的例子,经典模型就是消费者生产者,生产者者线程向队列里push数据,消费者线程从队列里pop数据。因为存在多线程内存共享的问题,因此必须考虑到要做保护,而简单操作就是加互斥锁,然而加了互斥锁,在实际运行代码时会发现CPU占用率高的问题,原因就在于,消费者线程(不止一个)会频繁判断队列是否为空,导致CPU做无用功。下面给一个博主的例子以及用条件变量优化解决后的CPU下降的对比结果。

参考互斥锁和条件变量的性能

利用互斥锁实现,使用纯互斥锁cpu的开销是很大的,main进程的cpu使用率达到了357.5%CPU,系统开销的cpu为54.5%sy,用户开销的cpu为18.2%us。

改用条件变量实现其CPU占用率几乎到了0%,虽然文中也给出了在互斥锁中加休眠来降低使用率,但是对于睡眠时间有要求。

[root@lincoding ~]# ps aux | grep -v grep  |grep main
USER        PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root      73838  0.0  0.1 141068  1256 pts/1    Sl+  19:54   0:00 ./main

最开始,我对于条件变量的理解只停留到了这个位置,可以应用到消费生产者模型,且能降低CPU高占用的问题,在正式开篇之前,我有必要介绍一下条件变量各个函数使用需要注意的地方。

条件变量

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

这种同步机制需要两个操作来配合同步实现:

  • 线程因为不满足条件变量的等待条件从而挂起  【condition_variable 对应的实现函数wait()、wait_for()、wait_until()】
  • 条件变量条件成立后,发出信号,被挂起线程被唤醒 【condition_variable 对应的实现函数notify_one()、notify_all()】

wait/wait_for

wait()函数可使得当前线程阻塞,直至条件变量唤醒、或是虚假唤醒。一般代码里为了避免虚假唤醒,会采用while()循环遍历条件的方式。

阻塞该线程时,该函数会自动解锁,允许其他线程执行。一旦得到notify唤醒,该函数取消阻塞并获取锁,然后函数返回,一般为了避免虚假唤醒,会用wile循环判断条件,若为该情况,则函数返回至whie循环条件处。

void wait( std::unique_lock<std::mutex>& lock );//Predicate 谓词函数,可以普通函数或者lambda表达式
template< class Predicate >
void wait( std::unique_lock<std::mutex>& lock, Predicate pred );//此处可调用lambda表达式返回false或者true值,若返回false则阻塞,返回true则取消阻塞//while(condition) wait(lock);
//wait(lock,[](){return !condition;})
//两者效果一致

wait_for()函数导致当前线程阻塞直至条件变量被通知、或者虚假唤醒发生,或者超时返回。相比于wait函数,此函数多了一个超时判断。

参考虚假唤醒

  1. 虚假唤醒(spurious wakeup)是一个表象,即在多处理器的系统下发出wait的程序有可能在没有notify唤醒的情形下苏醒继续执行。以运行在linux的hotspot虚拟机上的java程序为例,wait方法在jvm执行时实质是调用了底层pthread_cond_wait/pthread_cond_timedwait函数,挂起等待条件变量来达到线程间同步通信的效果,而底层wait函数在设计之初为了不减慢条件变量操作的效率并没有去保证每次唤醒都是由notify触发,而是把这个任务交由上层应用去实现,即使用者需要定义一个循环去判断是否条件真能满足程序继续运行的需求,当然这样的实现也可以避免因为设计缺陷导致程序异常唤醒的问题。参考链接:https://www.zhihu.com/question/50892224/answer/280667072

  2. 此外在生产者消费者模型中,若是存在多个消费者线程,若一个线程获取到锁执行流程,另一个线程在wait函数处阻塞,若是阻塞线程获取到锁,若是采用if条件判断,则阻塞线程会向下执行。假设上一个线程已经将数据去完,队列为空 ,那么这种情况就会出错。

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

notify_one/notify_all

notify_one函数,唤醒阻塞的线程之一。若无线程在等待,则该函数不执行任何操作,若线程超过一个,无法指定选择哪个线程。这里线程唤醒的先后顺序,个人怀疑与线程阻塞的先后顺序有关,若某线程先阻塞,则该线程先被唤醒。

notify_all函数,唤醒所有阻塞线程,若五线程等待,则该函数不执行任何操作。线程被全部唤醒,但是只有一个线程可以抢占到锁,拿到锁的线程先执行,后续线程谁先拿到锁,谁先执行,直到全部执行完毕。

参考notify_all函数具体使用细节

如果将notify_all改成notify(),则在有些情况下会导致并发不充分:假设在队列为空的情况下连续put几次,则只有第一次put唤醒了一个take线程。如果之后队列一直维持非空,则take线程们始终只有一个是活跃的。事情还可能更糟,如果掉进陷阱的线程非常关键,则系统可能陷入活锁状态。
周末的时候看了一篇不错的文章: 《Real-World Concurrency》,文中提到了问题的另一面:notify_all()容易导致惊群现象。这会导致系统性能的下降:假设某一时间段,take频率高于put,则队列始终趋近于空,但每次put都会唤醒所有阻塞在take的线程,但只有一个线程能继续运行。反之亦然。

总之: notify_all比较安全,较无心智负担,但可能带来明显的效率下降。
所以对于系统大部分模块来说,notify_all也够了,如果是基础设施模块,可以考虑用notify进行优化,但要小心。

细节理解

  • 为什么pthread_cond_wait需要加锁??###
  • 在生产者线程中修改条件时为什么要加mutex??###
  • 消费者线程中判断条件为什么要放在while中??###
  • signal到底是放在unlock之前还是之后??###

解决方案:

到此知识储备够了之后,离我解决bug也不远了。其实关键的地方可以再讲讲:

  • wait函数阻塞后,被其他线程唤醒后,该函数会返回,而为了避免虚假唤醒,利用while语句判断条件,该函数只返回至内层while循环处,无法返回至外层线程结束标志位循环处。
  • notify函数唤醒全部线程后,我之前误解以为,线程需要抢占锁,谁拿到锁,谁就执行一次,那么这次唤醒就相当于只唤醒了一次,而实际上的唤醒是将全部线程唤醒,线程按照通过锁竞争的方式依次执行完毕

针对以上,再我控件响应标志位后,我把线程标志位置位后,这里由于我的存数线程处于阻塞状态,无法判断线程标志位,所以我将调用一次notify_all函数并在内层的循环做条件判断,并利用goto语句跳转至外层循环做线程结束标志位判断,则线程无法结束的问题就解决了。

//存数线程执行内容也可以理解为消费者线程
TARGET_TXT:while(DataProcessRadar::STORE_READY_FLAG.TARGET_READY_FLAG)//通过标志位判断循环是否结束,该标志位受控件影响
{std::unique_lock<std::mutex> locker(mutex_txt);while(q_local_data_txt.empty())//线程唤醒后返回至此处{if(!DataProcessRadar::STORE_READY_FLAG.TARGET_READY_FLAG)//做判断goto至外层循环{goto TARGET_TXT;}cond_txt.wait(locker);}StoreData sd = q_local_data_txt.front();if(sd.type == Target)//队列中会存在两种数据类型,因此在此处会做判断,效率有点损失{q_local_data_txt.pop();locker.unlock();}
}
file.close();

为了模拟我的代码逻辑,我写了个简单测试demo。

首先我往队列里放入十个数,再启动两个消费者线程拿数,再启动一个线程延时一定时长后,置标志位,notify_all所有线程,正常退出线程。

#include <iostream>
#include <deque>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <Windows.h>std::deque<int> q;
std::mutex mu;
std::condition_variable cond;bool flag = true;
void function_1(int count) {// int count = 10;// while (count > 0) {std::unique_lock<std::mutex> locker(mu);q.push_front(count);locker.unlock();// cond.notify_one();// std::this_thread::sleep_for(std::chrono::seconds(1));// count--;// }
}void function_2() {int data = 0;label:while (flag) {std::cout << "in function_2 " << data << std::endl;std::unique_lock<std::mutex> locker(mu);while(q.empty()) {if(!flag) {std::cout<< "ending...function2"<< std::endl; goto label;}cond.wait(locker);}data = q.back();q.pop_back();// Sleep(1000);std::cout << "t2 got a value from t1: " << data << std::endl;locker.unlock();}std::cout << "t2 end!";
}void function_3() {int data = 0;label:while (flag) {std::cout << "in function_3 " << data << std::endl;std::unique_lock<std::mutex> locker(mu);while(q.empty()) {if(!flag) {std::cout<< "ending...function3"<< std::endl; goto label;}cond.wait(locker);}data = q.back();q.pop_back();// Sleep(1000);std::cout << "t3 got a value from t1: " << data << std::endl;locker.unlock();}std::cout << "t3 end!";
}void function_4()
{Sleep(5000);//延时flag = false;cond.notify_all();
}int main() {for(int i=0; i <= 10; i++){function_1(i);//往队列里放数}std::thread t2(function_2);//拿数std::thread t3(function_3);//拿数std::thread t4(function_4);t2.join();t3.join();return 0;
}

c++条件变量的使用详解以及wait()爬坑经历记录相关推荐

  1. 条件变量(condition variable)详解

    原理: 假设我们需要解决这样一个问题:一个列表记录需要处理的任务.一个线程往此列表添加任务,一个线程processTask处理此列表中的任务.这个问题的一个关键点在于processTask怎么判断任务 ...

  2. 适配iPhoneX全系详解,更新Xcode10爬坑

    前言 熬夜看了WWDC2018, 为了坚决响应苹果号召, 迅速贯彻落实iOS12的新系统, 公司组织决定让我作为一个排头兵, 更新Xcode10, 看看苹果的新思想, 新作为. 更新Xcode10带来 ...

  3. sumif三个条件怎么填_Excel条件求和函数sumif详解及应用

    Excel条件求和函数sumif详解及应用 相对于sum而言,sumif的便捷性有了很大拓展,可以根据指定的条件进行求和.这里对其参数进行介绍并加以实例演示. 1. 参数介绍 sumif(range, ...

  4. MybatisPlus学习(四)条件构造器Wrapper方法详解

    https://www.cnblogs.com/xianz666/p/13857733.html MybatisPlus学习(四)条件构造器Wrapper方法详解 文章目录 1.条件构造器 2.Que ...

  5. python跨函数调用变量_对python中不同模块(函数、类、变量)的调用详解

    首先,先介绍两种引入模块的方法. 法一:将整个文件引入 import 文件名 文件名.函数名( ) / 文件名.类名 通过这个方法可以运行另外一个文件里的函数 法二:只引入某个文件中一个类/函数/变量 ...

  6. java静态变量重复new_Java非静态成员变量之死循环(详解)

    1.非静态成员变量 当成员变量为非静态成员变量且对当前类进行实例化时,将会产生死循环 例子: public class ConstructorCls { private ConstructorCls ...

  7. java环境变量user.home详解

    java环境变量user.home详解 java user.home user.home含义 查看user.home 修改user.home log4j中使用 java user.home 今天梳理项 ...

  8. java使用变量输出_JAVA定义变量与输出详解

    一些重要知识 一个源文件里只能有一个public类,其它类数量不限.文件名与public类名相同 java程序严格区分大小写 JAVA应用程序的执行入口是main方法固定写法:public stati ...

  9. python 子字符串 位置_python查找子字符串位置Python变量和数据类型详解

    Python变量和数据类型 Python中数据类型 Python之print语句 Python的注释 Python中什么是变量 Python中定义字符串 Python中raw字符串与多行字符串 Pyt ...

最新文章

  1. 产品经理必备知识之网页设计系列(二)-如何设计出一个优秀的界面
  2. Linux操作(5)——创建硬链接与软链接
  3. 直接用自己服务器做图床可以吗_我花 9 块钱搭了一个“私人图床”
  4. C#2.0 从sql server 中读取二进制图片
  5. HihoCoder - 1175 拓扑排序·二
  6. oracle存储过程 ppt,oracle_存储过程培训(动画版本)详解.ppt
  7. VS2010自定义新建文件模版
  8. javaone_JavaOne 2012:101种改进Java的方法-开发人员参与为何如此重要
  9. JS factory
  10. **CI中使用IN查询(where_in)
  11. spark job运行参数优化
  12. 在线编辑Excel——插入图表
  13. vue 生成二维码(中间logo),下载二维码,复制链接(vue + vue-qr+clipboard)
  14. win10/win7安装Rational Rose 2007(解决虚拟光驱加载不了bin文件问题)
  15. 软件设计师中级-笔记
  16. 拆解一个老式电感电容表
  17. 基于强化学习的中间商赚差价指导手册
  18. 数字货币&区块链动态
  19. 企业微信网页授权及JS-SDK碰到检查域名所有权不通过的问题
  20. 诠释的伤感独特个性日志发布:你是今生最让我伤心的人

热门文章

  1. 打造国内专业企业研发管理解决方案,ONES 完成华创资本领投 A+轮 600 万美元融资...
  2. ps蒙版使用计算机一级,2020年计算机一级基础及Photoshop应用考试(另附题库)
  3. Android电子书集合
  4. 利用Google Sites制作个人主页
  5. 五款电竞选手最爱真无线蓝牙耳机,高续航随时随地畅玩手游
  6. 如何在html中加入动画效果,纯css如何实现按钮的水滴动画效果?
  7. 问题记录远程烧录和看门狗冲突
  8. 2022年亚洲最佳职场榜单公布,排名前30的跨国公司是这些 | 美通社头条
  9. 追根溯源-数据库deadlock重启
  10. Cydia sub-process /usr/libexec/cydia/cydo 报错 100