互斥锁std::mutex是一种最常见的线程间同步的手段,但是在有些情况下不太高效。

假设想实现一个简单的消费者生产者模型,一个线程往队列中放入数据,一个线程往队列中取数据,取数据前需要判断一下队列中确实有数据,由于这个队列是线程间共享的,所以,需要使用互斥锁进行保护,一个线程在往队列添加数据的时候,另一个线程不能取,反之亦然。用互斥锁实现如下:

#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));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();return 0;
}//输出结果
//t2 got a value from t1: 10
//t2 got a value from t1: 9
//t2 got a value from t1: 8
//t2 got a value from t1: 7
//t2 got a value from t1: 6
//t2 got a value from t1: 5
//t2 got a value from t1: 4
//t2 got a value from t1: 3
//t2 got a value from t1: 2
//t2 got a value from t1: 1

可以看到,互斥锁其实可以完成这个任务,但是却存在着性能问题。

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

CPU占用率.png

解决办法之一是给消费者也加一个小延时,如果一次判断后,发现队列是空的,就惩罚一下自己,延时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));}}
}

如图:

使用延时的CPU占用率.png

然后困难之处在于,如何确定这个延时时间呢,假如生产者生产的很快,消费者却延时500ms,也不是很好,如果生产者生产的更慢,那么消费者延时500ms,还是不必要的占用了CPU。

这就引出了条件变量(condition variable),c++11中提供了#include <condition_variable>头文件,其中的std::condition_variable可以和std::mutex结合一起使用,其中有两个重要的接口,notify_one()wait()wait()可以让线程陷入休眠状态,在消费者生产者模型中,如果生产者发现队列中没有东西,就可以让自己休眠,但是不能一直不干活啊,notify_one()就是唤醒处于wait中的其中一个条件变量(可能当时有很多条件变量都处于wait状态)。那什么时刻使用notify_one()比较好呢,当然是在生产者往队列中放数据的时候了,队列中有数据,就可以赶紧叫醒等待中的线程起来干活了。

使用条件变量修改后如下:

#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();  // Notify one waiting thread, if there is one.std::this_thread::sleep_for(std::chrono::seconds(1));count--;}
}void function_2() {int data = 0;while ( data != 1) {std::unique_lock<std::mutex> locker(mu);while(q.empty())cond.wait(locker); // Unlock mu and wait to be notifieddata = 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();return 0;
}

此时CPU的占用率也很低。

使用条件变量时的CPU占用率.png

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

  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没有lockunlock接口,而unique_lock提供了。这就是必须使用unique_lock的原因。
  3. 使用细粒度锁,尽量减小锁的范围,在notify_one()的时候,不需要处于互斥锁的保护范围内,所以在唤醒条件变量之前可以将锁unlock()

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

void function_2() {int data = 0;while ( data != 1) {std::unique_lock<std::mutex> locker(mu);cond.wait(locker, [](){ return !q.empty();} );  // Unlock mu and wait to be notifieddata = q.back();q.pop_back();locker.unlock();std::cout << "t2 got a value from t1: " << data << std::endl;}
}

除了notify_one()函数,c++还提供了notify_all()函数,可以同时唤醒所有处于wait状态的条件变量。

参考

  1. C++并发编程实战
  2. C++ Threading #6: Condition Variable

c++11 多线程编程(六)------条件变量(Condition Variable)相关推荐

  1. java suprious wakeup_多线程编程中条件变量和的spurious wakeup 虚假唤醒

    1. 概述 条件变量(condition variable)是利用共享的变量进行线程之间同步的一种机制.典型的场景包括生产者-消费者模型,线程池实现等. 对条件变量的使用包括两个动作: 1)线程等待某 ...

  2. 对条件变量(condition variable)的讨论

    作者:王东 1.1       什么是条件变量和条件等待? 简单的说: 条件变量(condition variable)是利用线程间共享的全局变量进行同步的一种机制,主要包括两个动作:一个线程等待某个 ...

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

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

  4. c++11多线程编程同步——使用条件变量condition variable

    简述 在多线程编程中,当多个线程之间需要进行某些同步机制时,如某个线程的执行需要另一个线程完成后才能进行,可以使用条件变量. c++11提供的 condition_variable 类是一个同步原语, ...

  5. C++多线程编程(2) 条件变量与原子操作

    条件变量 条件变量是c++11 提供的另一种用于等待的同步机制,它能阻塞一个或者多个线程,直到收到另外一个线程发出的通知或者超时,才会唤醒当前阻塞的线程,条件变量需要和互斥量配合使用,C++11提供两 ...

  6. python多线程编程(5): 条件变量同步

    From: http://www.cnblogs.com/holbrook/archive/2012/03/13/2394811.html 互斥锁是最简单的线程同步机制,Python提供的Condit ...

  7. Linux与C++11多线程编程(学习笔记)

    多线程编程与资源同步 在Windows下,主线程退出后,子线程也会被关闭; 在Linux下,主线程退出后,系统不会关闭子线程,这样就产生了僵尸进程 3.2.1创建线程 Linux 线程的创建 #inc ...

  8. Python 线程条件变量 Condition - Python零基础入门教程

    目录 一.Python 线程条件变量 Condition 函数 二.Python 线程条件变量 Condition 原理 三.Python 线程条件变量 Condition 使用 四.Python 线 ...

  9. python 线程超时设置_python 条件变量Condition(36)

    文章首发微信公众号,微信搜索:猿说python 对于线程与线程之间的交互我们在前面的文章已经介绍了 python 互斥锁Lock / python事件Event , 今天继续介绍一种线程交互方式 – ...

最新文章

  1. 同济大学土木工程学院招收2名秋季入学全日制博士生
  2. 2019年上半年计算机水平,2019年上半年全国计算机等级考试通过秘诀分享
  3. opencv图像处理学习(五十七)——峰值信噪比和结构相似性
  4. AHCI驱动中的cmd
  5. NBA球员数据的爬取
  6. javascript运算符——条件、逗号、赋值、()和void运算符 (转载)
  7. iOS App 签名的原理(转)
  8. 软件检测报告可用于即征即退政策
  9. 计算机一级电子表格TF函数,TFG1000系列DDS函数信号发生器基本操作
  10. Kafka SSL 和 ACL 配置
  11. 教师资格证面试计算机教案模板,教师资格证面试教案怎么写?教案模板拿走
  12. 随想录(sil、hil、mil测试)
  13. 使用busybox快速制作initramfs
  14. 页面局部刷新( ScriptManager 和 UpdatePanel)(转)
  15. 系统servlet、request知识付费(List/Map/Set)小程序开发
  16. 第三次作业 - 结对项目1
  17. 2022茶艺师(中级)考试题模拟考试题库及答案
  18. flask_萧井陌视频学习(二)
  19. 答辩PPT的美化以及配色
  20. Icon之线性图标设计指南

热门文章

  1. OSX下解决PIL的IOError: decoder jpeg not available 问题
  2. TechNet Plus订阅版再度推出6折优惠
  3. 问题二十:C++全局debug “ray tracing图形”实例
  4. 双电容单相电机接线图解_拿走不谢!75例自动控制原理图、接线图
  5. c++ 字符串拼接_python字符串零碎总结
  6. matlab的数据类型ppt,MATLAB数据类型
  7. AcWing 875. 快速幂
  8. 迪杰斯特拉算法 php,最短路径算法
  9. 手机 debian linux,Android上的Ubuntu Debian Armel
  10. Java构建工具:Maven与Gradle的对比