文章目录

  • 一、何为条件变量
  • 二、为何引入条件变量
  • 三、如何使用条件变量
  • 更多文章:

一、何为条件变量

在前一篇文章《C++多线程并发(二)—线程同步之互斥锁》中解释了线程同步的原理和实现,使用互斥锁解决数据竞争访问问题,算是线程同步的加锁原语,用于排他性的访问共享数据。我们在使用mutex时,一般都会期望加锁不要阻塞,总是能立刻拿到锁,然后尽快访问数据,用完之后尽快解锁,这样才能不影响并发性和性能。

如果需要等待某个条件的成立,我们就该使用条件变量(condition variable)了,那什么是条件变量呢,引用APUE中的一句话:

Condition variables are another synchronization mechanism available to threads.
These synchronization objects provide a place for threads to rendezvous. When used with mutexes, condition variables allow threads to wait in a race-free way for arbitrary conditions to occur.

条件变量是线程的另外一种有效同步机制。这些同步对象为线程提供了交互的场所(一个线程给另外的一个或者多个线程发送消息),我们指定在条件变量这个地方发生,一个线程用于修改这个变量使其满足其它线程继续往下执行的条件,其它线程则等待接收条件已经发生改变的信号。当条件变量同互斥锁一起使用时,条件变量允许线程以一种无竞争的方式等待任意条件的发生。

二、为何引入条件变量

前一章介绍了多线程并发访问共享数据时遇到的数据竞争问题,我们通过互斥锁保护共享数据,保证多线程对共享数据的访问同步有序。但如果一个线程需要等待一个互斥锁的释放,该线程通常需要轮询该互斥锁是否已被释放,我们也很难找到适当的轮训周期,如果轮询周期太短则太浪费CPU资源,如果轮询周期太长则可能互斥锁已被释放而该线程还在睡眠导致发生延误。

下面给出一个简单的程序示例:一个线程往队列中放入数据,一个线程从队列中提取数据,取数据前需要判断一下队列中确实有数据,由于这个队列是线程间共享的,所以,需要使用互斥锁进行保护,一个线程在往队列添加数据的时候,另一个线程不能取,反之亦然。程序实现代码如下:

//cond_var1.cpp用互斥锁实现一个生产者消费者模型#include <iostream>
#include <deque>
#include <thread>
#include <mutex>std::deque<int> q;                      //双端队列标准容器全局变量
std::mutex mu;                          //互斥锁全局变量
//生产者,往队列放入数据
void function_1() {int count = 10;while (count > 0) {std::unique_lock<std::mutex> locker(mu);q.push_front(count);         //数据入队锁保护locker.unlock();std::this_thread::sleep_for(std::chrono::seconds(1));      //延时1秒count--;}
}
//消费者,从队列提取数据
void function_2() {int data = 0;while ( data != 1) {std::unique_lock<std::mutex> locker(mu);if (!q.empty()) {           //判断队列是否为空data = q.back();q.pop_back();            //数据出队锁保护locker.unlock();std::cout << "t2 got a value from t1: " << data << std::endl;} else {locker.unlock();}}
}int main() {std::thread t1(function_1);std::thread t2(function_2);t1.join();t2.join();getchar();return 0;
}

程序执行结果如下:

从代码中不难看出:在生产过程中,因每放入一个数据有1秒延时,所以这个生产的过程是很慢的;在消费过程中,存在着一个while循环,只有在接收到表示结束的数据的时候,才会停止,每次循环内部,都是先加锁,判断队列不空,然后就取出一个数,最后解锁。所以说,在1s内,做了很多无用功!这样的话,CPU占用率会很高,可能达到100%(单核)。如下图示:

既然是由于消费者在while循环内因等待数据做了过多的无用功导致CPU占有率过高,我们可以考虑在消费者发现队列为空时,让消费者小睡一会儿,即增加一个小延时(比如500ms),相当于增大了轮询间隔周期,应该能降低CPU的占用率。按该方案修改后的消费者代码如下:

//消费者,从队列提取数据
void function_2() {int data = 0;while ( data != 1) {std::unique_lock<std::mutex> locker(mu);if (!q.empty()) {           //判断队列是否为空data = q.back();q.pop_back();            //数据出队锁保护locker.unlock();std::cout << "t2 got a value from t1: " << data << std::endl;} else {locker.unlock();std::this_thread::sleep_for(std::chrono::milliseconds(500));      //延时500毫秒}}
}

增大轮询周期后,CPU占有率下降很明显:

但前面也说了,困难之处在于如何确定这个延长时间(即轮询间隔周期),如果间隔太短会过多占用CPU资源,如果间隔太长会因无法及时响应造成延误。

这就引入了条件变量来解决该问题:条件变量使用“通知—唤醒”模型,生产者生产出一个数据后通知消费者使用,消费者在未接到通知前处于休眠状态节约CPU资源;当消费者收到通知后,赶紧从休眠状态被唤醒来处理数据,使用了事件驱动模型,在保证不误事儿的情况下尽可能减少无用功降低对资源的消耗。

三、如何使用条件变量

C++标准库在< condition_variable >中提供了条件变量,借由它,一个线程可以唤醒一个或多个其他等待中的线程。原则上,条件变量的运作如下:

  • 你必须同时包含< mutex >和< condition_variable >,并声明一个mutex和一个condition_variable变量;
  • 那个通知“条件已满足”的线程(或多个线程之一)必须调用notify_one()或notify_all(),以便条件满足时唤醒处于等待中的一个条件变量;
  • 那个等待"条件被满足"的线程必须调用wait(),可以让线程在条件未被满足时陷入休眠状态,当接收到通知时被唤醒去处理相应的任务;

将上面的cond_var1.cpp程序使用条件变量解决轮询间隔难题的示例代码如下:

//cond_var2.cpp用条件变量解决轮询间隔难题#include <iostream>
#include <deque>
#include <thread>
#include <mutex>
#include <condition_variable>std::deque<int> q;                     //双端队列标准容器全局变量
std::mutex mu;                          //互斥锁全局变量
std::condition_variable cond;           //全局条件变量
//生产者,往队列放入数据
void function_1() {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));        //延时1秒count--;}
}
//消费者,从队列提取数据
void function_2() {int data = 0;while ( data != 1) {std::unique_lock<std::mutex> locker(mu);while(q.empty())        //判断队列是否为空cond.wait(locker); // 解锁互斥量并陷入休眠以等待通知被唤醒,被唤醒后加锁以保护共享数据data = q.back();q.pop_back();           //数据出队锁保护locker.unlock();std::cout << "t2 got a value from t1: " << data << std::endl;}
}int main() {std::thread t1(function_1);std::thread t2(function_2);t1.join();t2.join();getchar();return 0;
}

使用条件变量对CPU的占用率也很低,而且免去了轮询间隔该设多长的难题:

上面的代码有三个注意事项:

  1. 在function_2中,在判断队列是否为空的时候,使用的是while(q.empty()),而不是if(q.empty()),这是因为wait()从阻塞到返回,不一定就是由于notify_one()函数造成的,还有可能由于系统的不确定原因唤醒(可能和条件变量的实现机制有关),这个的时机和频率都是不确定的,被称作伪唤醒。如果在错误的时候被唤醒了,执行后面的语句就会错误,所以需要再次判断队列是否为空,如果还是为空,就继续wait()阻塞;
  2. 在管理互斥锁的时候,使用的是std::unique_lock而不是std::lock_guard,而且事实上也不能使用std::lock_guard。这需要先解释下wait()函数所做的事情,可以看到,在wait()函数之前,使用互斥锁保护了,如果wait的时候什么都没做,岂不是一直持有互斥锁?那生产者也会一直卡住,不能够将数据放入队列中了。所以,wait()函数会先调用互斥锁的unlock()函数,然后再将自己睡眠,在被唤醒后,又会继续持有锁,保护后面的队列操作。lock_guard没有lock和unlock接口,而unique_lock提供了,这就是必须使用unique_lock的原因;
  3. 使用细粒度锁,尽量减小锁的范围,在notify_one()的时候,不需要处于互斥锁的保护范围内,所以在唤醒条件变量之前可以将锁unlock()。

还可以将cond.wait(locker)换一种写法,wait()的第二个参数可以传入一个函数表示检查条件,这里使用lambda函数最为简单,如果这个函数返回的是true,wait()函数不会阻塞会直接返回,如果这个函数返回的是false,wait()函数就会阻塞着等待唤醒,如果被伪唤醒,会继续判断函数返回值。代码示例如下:

//消费者,从队列提取数据
void function_2() {int data = 0;while ( data != 1) {std::unique_lock<std::mutex> locker(mu);cond.wait(locker, [](){ return !q.empty();});   //如果条件变量被唤醒,检查队列非空条件是否为真,为真则直接返回,为假则继续等待data = q.back();q.pop_back();         //数据出队锁保护locker.unlock();std::cout << "t2 got a value from t1: " << data << std::endl;}
}

下面给出条件变量支持的操作函数表:

值得注意的是:

  • 所有通知(notification)都会被自动同步化,所以并发调用notify_one()和notify_all()不会带来麻烦;
  • 所有等待某个条件变量(condition variable)的线程都必须使用相同的mutex,当wait()家族的某个成员被调用时该mutex必须被unique_lock锁定,否则会发生不明确的行为;
  • wait()函数会执行“解锁互斥量–>陷入休眠等待–>被通知唤醒–>再次锁定互斥量–>检查条件判断式是否为真”几个步骤,这意味着传给wait函数的判断式总是在锁定情况下被调用的,可以安全的处理受互斥量保护的对象;但在"解锁互斥量–>陷入休眠等待"过程之间产生的通知(notification)会被遗失。

线程同步保证了多个线程对共享数据的有序访问,目前我们了解到的多线程间传递数据主要是通过共享数据(全局变量)实现的,全局共享变量的使用容易增加不同任务或线程间的耦合度,也增加了引入bug的风险,所以全局共享变量应尽可能少用。很多时候我们只需要传递某个线程或任务的执行结果,以便参与后续的运算,但我们又不想阻塞等待该线程或任务执行完毕,而是继续执行暂时不需要该线程或任务执行结果参与的运算,当需要该线程执行结果时直接获得,才能更充分发挥多线程并发的效率优势。想了解该问题,请继续阅读下一篇文章:《C++多线程并发(四)—异步编程》。

更多文章:

  • 《C++多线程并发—本章GitHub源码》
  • 《C++多线程并发(一)— 线程创建与管理》
  • 《C++多线程并发(二)—线程同步之互斥锁》
  • 《C++多线程并发(四)—异步编程》
  • 《C++多线程并发(五)—原子操作与无锁编程》
  • 《C++ Concurrency in Action》
  • 《C++线程支持库》

C++多线程并发(三)---线程同步之条件变量相关推荐

  1. Linux——线程同步(条件变量、POSIX信号量)和线程池

    一.线程同步 (一).概念 线程同步是一种多线程关系,指的是线程之间按照特定顺序访问临界资源,进而能够避免线程饥饿问题. 所谓线程饥饿指的是某个线程长期"霸占"临界资源,导致其他线 ...

  2. 线程同步之条件变量:pthread_cond_signal和pthread_cond_wait

    在多线程编程下,常常出现A线程要等待B线程条件完成后再继续进行,这里等待方式有两种: 1.使用锁+轮询 使用这种方法可以很简单的实现,但是会有一定的性能消耗,其还有一个点要好好把握,就是一次轮询没有结 ...

  3. Linux线程同步之条件变量

    与互斥锁不同,条件变量是用来等待而不是用来上锁的.条件变量用来自动阻塞一个线程,直到某特殊情况发生为止.通常条件变量和互斥锁同时使用. 条件变量使我们可以睡眠等待某种条件出现.条件变量是利用线程间共享 ...

  4. [转]Linux线程同步之条件变量

    与互斥锁不同,条件变量是用来等待而不是用来上锁的.条件变量用来自动阻塞一个线程,直到某特殊情况发生为止.通常条件变量和互斥锁同时使用. 条件变量使我们可以睡眠等待某种条件出现.条件变量是利用线程间共享 ...

  5. C++多线程学习(三)——线程同步之条件变量

    前面学习了互斥量似乎我们就可以多线程编程了,多线程也不过如此嘛.然而我们上手coding,用多线程来结局我们实际需求就会发现,似乎多线程也不是很好用.因为我们实际对于多线程的需求,往往线程都是whil ...

  6. Linux系统编程38:多线程之什么是线程同步以及条件变量函数

    文章目录 (1):什么是线程的同步 (2):实现线程同步-条件变量函数 (1):什么是线程的同步 假如有一片临界资源,线程A和B都会修改它,为了保护资源所以要加锁,此时它们之间是互斥的关系.在我们的代 ...

  7. linux线程同步(2)-条件变量

    一.概述                                                    上一篇,介绍了互斥量.条件变量与互斥量不同,互斥量是防止多线程同时访问共享的互斥变量来保 ...

  8. UNIX环境高级编程——线程同步之条件变量以及属性

    条件变量变量也是出自POSIX线程标准,另一种线程同步机制.主要用来等待某个条件的发生.可以用来同步同一进程中的各个线程.当然如果一个条件变量存放在多个进程共享的某个内存区中,那么还可以通过条件变量来 ...

  9. linux 条件变量函数,Linux线程同步之条件变量

    条件变量变量也是出自POSIX线程标准,另一种线程同步机制,.主要用来等待某个条件的发生.可以用来同步同一进程中的各个线程.当然如果一个条件变量存放在多个进程共享的某个内存区中,那么还可以通过条件变量 ...

最新文章

  1. 【华为】华为模拟器模拟静态、动态NAT、PAT技术
  2. BZOJ2005: [Noi2010]能量采集 莫比乌斯反演的另一种方法——nlogn筛
  3. c#事件,委托机制(转)
  4. web前端自学入门视频教程分享
  5. shell 脚本比较字符串相等_LINUX快速入门第十六章:Shell 流程控制
  6. centos7配置IP地址CentOS7 修改hostname,ip地址以及hosts(永久生效)
  7. CentOS学习笔记 - 7. jekins安装 1
  8. 基于Java+SpringBoot+vue+node.js的智能农场管理系统详细设计和实现
  9. 电脑没声音,音频设备无法使用。扬声器安装程序unknown
  10. 泰坦尼克数据集kaggle Titanic下载
  11. 【随笔】理性国度的感性之火——浅论《报任安书》
  12. html竖线分割符的特殊符号,网站标题用什么分隔符号
  13. 【数据可视化】360度教你如何全面学习可视化——上篇
  14. esp32-cam摄像头+远程遥控小车
  15. php的mvc设计模式,什么是MVC设计模式?,
  16. 使用 acme.sh 签发 SSL证书失败
  17. 【Leetcode】1137. 第 N 个泰波那契数
  18. 下一代iPhone什么样
  19. Cannot write to ‘’ (Success) 解决办法
  20. Linux—vmstat命令详解

热门文章

  1. 《Effective Objective-C 2.0》—(第47-52条)—系统架构,foundation、快速遍历for-in、NSTimer
  2. 解决Parallels Desktop Windows11虚拟机上传网速变慢方法
  3. SAP Marketing Cloud Restful API SDK 使用案例分享
  4. Linux查看用户列表
  5. MacBook安装Redis
  6. MySQL的安装(详细教程)
  7. Java小项目超市管理系统(数组实现)
  8. Leetcode Nim 游戏
  9. DIV css中cursor属性详解-鼠标移到图片变换鼠标形状 (转)
  10. 拜读滴滴D8黄轶大神慕课网vue2.0APP教程感想