博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
[c++11]多线程编程(六)——条件变量(Condition Variable)
阅读量:6847 次
发布时间:2019-06-26

本文共 4444 字,大约阅读时间需要 14 分钟。

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

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

#include 
#include
#include
#include
std::deque
q;std::mutex mu;void function_1() { int count = 10; while (count > 0) { std::unique_lock
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
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
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 
#include
#include
#include
#include
std::deque
q;std::mutex mu;std::condition_variable cond;void function_1() { int count = 10; while (count > 0) { std::unique_lock
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
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
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状态的条件变量。

参考

转载地址:http://oflul.baihongyu.com/

你可能感兴趣的文章
Java访问Url地址并下载文件
查看>>
I.MX6 默认打开 USB adb
查看>>
【转】iOS类似Android上toast效果
查看>>
Study WP7 2:[Forward]:All about WP7 Isolated Storage
查看>>
到底是为什么而读书?一名武汉大学毕业生的反思
查看>>
php apache ubuntu 转
查看>>
htm l img
查看>>
提高SQL查询效率
查看>>
jquery星级评论打分组件
查看>>
Ruby uuidtools
查看>>
VC API常用函数简单例子大全(1-89)
查看>>
Eclipse快捷键 10个最有用的快捷键
查看>>
JavaScript中的 Dictionary 的用法
查看>>
代码腐化之路
查看>>
Linux下批量转换文件编码
查看>>
2.18 数组分割
查看>>
ubuntu rails3.2.16 提示gem mysql adapter
查看>>
C语言字符串长度(转)
查看>>
数据库索引的作用和长处缺点
查看>>
处理内容有&特殊字符thinkphp返回xml无法解析的问题<![CDATA[xxx]]>
查看>>