[c++11]多线程编程(六)——条件变量(Condition Variable)-程序员宅基地

技术标签: unique_lock  c++11  condition_variable  

转自:https://www.jianshu.com/p/c1dfa1d40f53

互斥锁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 notified
        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();
    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 notified
        data = q.back();
        q.pop_back();
        locker.unlock();
        std::cout << "t2 got a value from t1: " << data << std::endl;
    }
}

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

 

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/chm880910/article/details/86706255

智能推荐

随笔——history跳转页面后,获取参数_antd history获取参数-程序员宅基地

跳转页面通过history.push({ pathname: '/intent/handle/ProcessIntentOrder', query: { id: record.id, checkFlag: false } });// 到跳转页面后const { query = {} } = props.location;console.log(query.checkFlag, '查看还是处理'); // 获取到的会是一个字符串false,所以最好记得是将checkFlag:_antd history获取参数

** (java:10104): WARNING **: Could not open X display (MobaXterm无法打开smartgit)-程序员宅基地

1.问题平常都是windows这台主机通过MobaXterm远程到另外一台Ubuntu主机,今天突然smargit无法使用。报错:root@chenwr-pc:/home/workspace/project/grpc_project/build_ec20_grpc_demo# smartgit** (java:10104): WARNING **: Could not open X displayError invoking SmartGitjava.lang.reflect.Invocati

sdut (2080)最长公共子序列问题_最长公共子序列代码中的memset(c,0,sizeof(c))是什么意思-程序员宅基地

最长公共子序列问题Time Limit: 1000 ms Memory Limit: 65536 KiBSubmit StatisticProblem Description给定两个序列 X={x1,x2,…,xm} 和 Y={y1,y2,…,yn},找出X和Y的最长公共子序列。Input输入数据有多组,每组有两行 ,每行为一个长度不超过500的字符串(输入全是大写英文字母(A,Z))..._最长公共子序列代码中的memset(c,0,sizeof(c))是什么意思

搭建一个laravel 8_laravel8 resource-程序员宅基地

一、搭建基础laravel功能(持续更新)1.创建基础环境php artisan make:model SphericalPicturephp artisan make:controller --api -m "App\Models\SphericalPicture" SphericalPictureControllerphp artisan make:resource -c SphericalPictureCollectionsphp artisan make:resource Spherica_laravel8 resource

服务器被入侵了怎么办?-程序员宅基地

遇到服务器被黑,很多人会采用拔网线、封 iptables 或者关掉所有服务的方式应急,但如果是线上服务器就不能立即采用任何影响业务的手段了,需要根据服务器业务情况分类处理。下面我们看一个标准的服务器安全应急影响应该怎么做,也算是笔者从事安全事件应急近 6 年以来的一些经验之谈,借此抛砖引玉,希望大神们不吝赐教。图 1:处理思路如上图,将服务器安全应急响应流程分为如下 8 个环节:发现安全事件(核实

浅谈,如何获取MTK CPU信息 请看我是如何做的。一步步来吧_mtk处理器读取-程序员宅基地

很多时候,我们在想,如果想,写一个模块,能够去更方便,更强大的_mtk处理器读取

随便推点

springcloudalibaba与各组件版本对应说明_springcloudalibaba2.2.7对应springcloud什么版本-程序员宅基地

访问一下网址:版本说明 · alibaba/spring-cloud-alibaba Wiki · GitHub_springcloudalibaba2.2.7对应springcloud什么版本

最实用的简历模板之一:QQ邮箱免费简历模板_qq邮箱简历模板_假期的学习的博客-程序员宅基地

一、 使用地址https://mail.qq.com/二、 使用方法图一:点击简历图二:点击图一点击后的页面的右上角:新建模板图三:上一步的效果如图图四:现在可以根据自己需要自定义简历了三、最后的这个简历当然不一定是最好的,但是能快速自定义一个相对于好一些的简历也还好。还有那最后的那个自定义模块,可以根据需要自定义自己的模块。..._qq邮箱简历模板

那些令程序员泪流满面的瞬间-程序员宅基地

来自:程序员最幽默程序员这个职业,说的牛逼点,软件开发工程师,改变这个世界,说的诙谐点,就是码农,经常加班,周末大部分程序员都是宅在家里,哪也不想去(有的周末仍在加班)。1.明明我只...

/usr/bin/xauth: file /home/wj/.Xauthority does not exist_/usr/bin/xauth: file /root/.xauthority does not ex_一枚小爪哇的博客-程序员宅基地

解决【/usr/bin/xauth: file /home/wj/.Xauthority does not exist】xshell登录Ubuntu远程ip报错错误原因:是因为添加用户时没有授权对应的目录,仅仅执行了useradd user而没有授权对应的目录,直接解决办法如下(执行如下命令,以后就登录到终端上就不会出现上面的错误信息)chown username:username -R /home/user_dir我的目录是/home/wjusername是wj执行命令后再次登录就_/usr/bin/xauth: file /root/.xauthority does not exist

Jarvis-拍立淘里面的深度学习引擎-程序员宅基地

介绍深度学习的原理?局部响应归一化的作用?兄弟今天不是来讨论这个的,那都是科学家和算法同学的事儿。作为一个深度学习引擎,使命只有一个——就是快速和准确的计算。那怎么才能快呢?在编程技能和算法技能没有蜕变的情况下,使用GPU是一个方便的法门。对于iOS来说,就是使用Metal,对于iOS10+iPhone6以上,那就更厉害了,可以使用Metal Perf...

ElasticSearch7集群搭建(Windows单机版)_windows 单机部署 elasticsearch7 并环境配置-程序员宅基地

ElasticSearch名为分布式搜索引擎,所以为了实现高可用,ElasticSearch一般是要搭建集群环境的。 这里我们只在一台电脑上搭建三个ElasticSearch客户端来建立伪分布式就可以开始我们的学习之旅了。一.解压压缩包我们直接多解压两份elasticsearch-7.4.2-windows-x86_64.zip压缩包再修改名字就行了:二.修改配置文件..._windows 单机部署 elasticsearch7 并环境配置