C++ 并发编程之互斥锁和条件变量的性能比较
介绍
本文以最简单生产者消费者模型,通过运行程序,观察该进程的cpu使用率,来对比使用互斥锁 和 互斥锁+条件变量的性能比较。
本例子的生产者消费者模型,1个生产者,5个消费者。
生产者线程往队列里放入数据,5个消费者线程从队列取数据,取数据前需要判断一下队列中是否有数据,这个队列是全局队列,是线程间共享的数据,所以需要使用互斥锁进行保护。即生产者在往队列里放入数据时,其余消费者不能取,反之亦然。
互斥锁实现的代码
#include <iostream> // std::cout
#include <deque> // std::deque
#include <thread> // std::thread
#include <chrono> // std::chrono
#include <mutex> // std::mutex
// 全局队列
std::deque<int> g_deque;
// 全局锁
std::mutex g_mutex;
// 生产者运行标记
bool producer_is_running = true;
// 生产者线程函数
void Producer()
{
// 库存个数
int count = 8;
do
{
// 智能锁,初始化后即加锁,保护的范围是代码花括号内,花括号退出即会自动解锁
// 可以手动解锁,从而控制互斥锁的细粒度
std::unique_lock<std::mutex> locker( g_mutex );
// 入队一个数据
g_deque.push_front( count );
// 提前解锁,缩小互斥锁的细粒度,只针对共享的队列数据进行同步保护
locker.unlock();
std::cout << "生产者 :我现在库存有 :" << count << std::endl;
// 放慢生产者生产速度,睡1秒
std::this_thread::sleep_for( std::chrono::seconds( 1 ) );
// 库存自减少
count--;
} while( count > 0 );
// 标记生产者打样了
producer_is_running = false;
std::cout << "生产者 : 我的库存没有了,我要打样了!" << std::endl;
}
// 消费者线程函数
void Consumer(int id)
{
int data = 0;
do
{
std::unique_lock<std::mutex> locker( g_mutex );
if( !g_deque.empty() )
{
data = g_deque.back();
g_deque.pop_back();
locker.unlock();
std::cout << "消费者[" << id << "] : 我抢到货的编号是 :" << data << std::endl;
}
else
{
locker.unlock();
}
} while( producer_is_running );
std::cout << "消费者[" << id << "] :卖家没有货打样了,真可惜,下次再来抢!" << std::endl;
}
int main(void)
{
std::cout << "1 producer start ..." << std::endl;
std::thread producer( Producer );
std::cout << "5 consumer start ..." << std::endl;
std::thread consumer[ 5 ];
for(int i = 0; i < 5; i++)
{
consumer[i] = std::thread(Consumer, i + 1);
}
producer.join();
for(int i = 0; i < 5; i++)
{
consumer[i].join();
}
std::cout << "All threads joined." << std::endl;
return 0;
}
互斥锁实现运行结果:
结果输出
[root@lincoding condition]# g++ -std=c++0x -pthread -D_GLIBCXX_USE_NANOSLEEP main.cpp -o main
[root@lincoding condition]# ./main
1 producer start ...
5 consumer start ...
生产者 :我现在库存有 :8
消费者[1] : 我抢到货的编号是 :8
消费者[1] : 我抢到货的编号是 :7
生产者 :我现在库存有 :7
生产者 :我现在库存有 :6
消费者[3] : 我抢到货的编号是 :6
生产者 :我现在库存有 :5
消费者[1] : 我抢到货的编号是 :5
生产者 :我现在库存有 :4
消费者[2] : 我抢到货的编号是 :4
生产者 :我现在库存有 :3
消费者[5] : 我抢到货的编号是 :3
生产者 :我现在库存有 :2
消费者[2] : 我抢到货的编号是 :2
生产者 :我现在库存有 :1
消费者[1] : 我抢到货的编号是 :1
生产者 : 我的库存没有了,我要打样了!消费者[
5] :卖家没有货打样了,真可惜,下次再来抢!
消费者[2] :卖家没有货打样了,真可惜,下次再来抢!
消费者[3] :卖家没有货打样了,真可惜,下次再来抢!
消费者[4] :卖家没有货打样了,真可惜,下次再来抢!
消费者[1] :卖家没有货打样了,真可惜,下次再来抢!
All threads joined.
可以看到,互斥锁其实可以完成这个任务,但是却存在着性能问题。
Producer是生产者线程,在生产者数据过程中,会休息1秒,所以这个生产过程是很慢的;Consumer是消费者线程,存在着一个while循环,只有判断到生产者不运行了,才会退出while循环,那么每次在循环体内,都是会先加锁,判断队列不空,然后从列队取出一个数据,最后解锁。所以说,在生产者休息1秒的时候,消费者线程实际上会做很多无用功,导致CPU使用率非常高!
运行的环境是4核cpu
[root@lincoding ~]# grep 'model name' /proc/cpuinfo | wc -l
4
top命令查看cpu使用情况,可见使用纯互斥锁cpu的开销是很大的,main进程的cpu使用率达到了357.5%CPU,系统开销的cpu为54.5%sy,用户开销的cpu为18.2%us
[root@lincoding ~]# top
top - 19:13:41 up 36 min, 3 users, load average: 0.06, 0.05, 0.01
Tasks: 179 total, 1 running, 178 sleeping, 0 stopped, 0 zombie
Cpu(s): 18.2%us, 54.5%sy, 0.0%ni, 27.3%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st
Mem: 1004412k total, 313492k used, 690920k free, 41424k buffers
Swap: 2031608k total, 0k used, 2031608k free, 79968k cached
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
35346 root 20 0 137m 3288 1024 S 357.5 0.3 0:05.92 main
1 root 20 0 19232 1492 1224 S 0.0 0.1 0:02.16 init
2 root 20 0 0 0 0 S 0.0 0.0 0:00.01 kthreadd
3 root RT 0 0 0 0 S 0.0 0.0 0:00.68 migration/0
解决的办法之一就是给消费者也加一个小延时,当消费者没取到数据时,就休息一下500毫秒,这样可以减少互斥锁给cpu带来的开销。
// 消费者线程函数
void Consumer(int id)
{
int data = 0;
do
{
std::unique_lock<std::mutex> locker( g_mutex );
if( !g_deque.empty() )
{
data = g_deque.back();
g_deque.pop_back();
locker.unlock();
std::cout << "消费者[" << id << "] : 我抢到货的编号是 :" << data << std::endl;
}
else
{
locker.unlock();
// 当消费者没取到数据时,就休息一下500毫秒
std::this_thread::sleep_for( std::chrono::milliseconds( 500 ) );
}
} while( producer_is_running );
std::cout << "消费者[" << id << "] :卖家没有货打样了,真可惜,下次再来抢!" << std::endl;
}
从运行结果可知,cpu使用率大大降低了
[root@lincoding ~]# ps aux | grep -v grep |grep main
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 61296 0.0 0.1 141068 1244 pts/1 Sl+ 19:40 0:00 ./main
条件变量+互斥锁实现的代码
那么问题来了,如何确定消费者延时(休息)多久呢?
- 如果生产者生产的非常快,消费者却延时了
500毫秒,也不是很好 - 如果生产者生产的更慢,那么消费延时
500毫秒,也会有无用功,占用了CPU
这就需要引入条件变量std::condition_variable,应用于消费者生产模型中,就是生产者生产完一个数据后,通过notify_one()唤醒正在wait()消费者线程,使得消费者从队列取出一个数据。
#include <iostream> // std::cout
#include <deque> // std::deque
#include <thread> // std::thread
#include <chrono> // std::chrono
#include <mutex> // std::mutex
#include <condition_variable> // std::condition_variable
// 全局队列
std::deque<int> g_deque;
// 全局锁
std::mutex g_mutex;
// 全局条件变量
std::condition_variable g_cond;
// 生产者运行标记
bool producer_is_running = true;
// 生产者线程函数
void Producer()
{
// 库存个数
int count = 8;
do
{
// 智能锁,初始化后即加锁,保护的范围是代码花括号内,花括号退出即会自动解锁
// 可以手动解锁,从而控制互斥锁的细粒度
std::unique_lock<std::mutex> locker( g_mutex );
// 入队一个数据
g_deque.push_front( count );
// 提前解锁,缩小互斥锁的细粒度,只针对共享的队列数据进行同步保护
locker.unlock();
std::cout << "生产者 :我现在库存有 :" << count << std::endl;
// 唤醒一个线程
g_cond.notify_one();
// 睡1秒
std::this_thread::sleep_for( std::chrono::seconds( 1 ) );
// 库存自减少
count--;
} while( count > 0 );
// 标记生产者打样了
producer_is_running = false;
// 唤醒所有消费线程
g_cond.notify_all();
std::cout << "生产者 : 我的库存没有了,我要打样了!" << std::endl;
}
// 消费者线程函数
void Consumer(int id)
{
// 购买的货品编号
int data = 0;
do
{
// 智能锁,初始化后即加锁,保护的范围是代码花括号内,花括号退出即会自动解锁
// 可以手动解锁,从而控制互斥锁的细粒度
std::unique_lock<std::mutex> locker( g_mutex );
// wait()函数会先调用互斥锁的unlock()函数,然后再将自己睡眠,在被唤醒后,又会继续持有锁,保护后面的队列操作
// 必须使用unique_lock,不能使用lock_guard,因为lock_guard没有lock和unlock接口,而unique_lock则都提供了
g_cond.wait(locker);
// 队列不为空
if( !g_deque.empty() )
{
// 取出队列里最后一个数据
data = g_deque.back();
// 删除队列里最后一个数据
g_deque.pop_back();
// 提前解锁,缩小互斥锁的细粒度,只针对共享的队列数据进行同步保护
locker.unlock();
std::cout << "消费者[" << id << "] : 我抢到货的编号是 :" << data << std::endl;
}
// 队列为空
else
{
locker.unlock();
}
} while( producer_is_running );
std::cout << "消费者[" << id << "] :卖家没有货打样了,真可惜,下次再来抢!" << std::endl;
}
int main(void)
{
std::cout << "1 producer start ..." << std::endl;
std::thread producer( Producer );
std::cout << "5 consumer start ..." << std::endl;
std::thread consumer[ 5 ];
for(int i = 0; i < 5; i++)
{
consumer[i] = std::thread(Consumer, i + 1);
}
producer.join();
for(int i = 0; i < 5; i++)
{
consumer[i].join();
}
std::cout << "All threads joined." << std::endl;
return 0;
}
条件变量+互斥锁运行结果
[root@lincoding condition]# g++ -std=c++0x -pthread -D_GLIBCXX_USE_NANOSLEEP main.cpp -o main
[root@lincoding condition]#
[root@lincoding condition]# ./main
1 producer start ...
5 consumer start ...
生产者 :我现在库存有 :8
消费者[4] : 我抢到货的编号是 :8
生产者 :我现在库存有 :7
消费者[2] : 我抢到货的编号是 :7
生产者 :我现在库存有 :6
消费者[3] : 我抢到货的编号是 :6
生产者 :我现在库存有 :5
消费者[5] : 我抢到货的编号是 :5
生产者 :我现在库存有 :4
消费者[1] : 我抢到货的编号是 :4
生产者 :我现在库存有 :3
消费者[4] : 我抢到货的编号是 :3
生产者 :我现在库存有 :2
消费者[2] : 我抢到货的编号是 :2
生产者 :我现在库存有 :1
消费者[3] : 我抢到货的编号是 :1
生产者 : 我的库存没有了,我要打样了!
消费者[5] :卖家没有货打样了,真可惜,下次再来抢!
消费者[1] :卖家没有货打样了,真可惜,下次再来抢!
消费者[4] :卖家没有货打样了,真可惜,下次再来抢!
消费者[2] :卖家没有货打样了,真可惜,下次再来抢!
消费者[3] :卖家没有货打样了,真可惜,下次再来抢!
All threads joined.
CPU开销非常的小
[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的性能开销非常大,可以使用互斥锁+条件变量的方式,当生产者线程生产了一个数据,就唤醒消费者线程进行消费,避免一些无用功的性能开销。
C++ 并发编程之互斥锁和条件变量的性能比较的更多相关文章
- 【Linux C 多线程编程】互斥锁与条件变量
一.互斥锁 互斥量从本质上说就是一把锁, 提供对共享资源的保护访问. 1) 初始化: 在Linux下, 线程的互斥量数据类型是pthread_mutex_t. 在使用前, 要对它进行初始化: 对于静态 ...
- python 并发编程 多进程 互斥锁 目录
python 并发编程 多进程 互斥锁 模拟抢票 互斥锁与join区别
- linux c 线程间同步(通信)的几种方法--互斥锁,条件变量,信号量,读写锁
Linux下提供了多种方式来处理线程同步,最常用的是互斥锁.条件变量.信号量和读写锁. 下面是思维导图: 一.互斥锁(mutex) 锁机制是同一时刻只允许一个线程执行一个关键部分的代码. 1 . ...
- linux 线程的同步 二 (互斥锁和条件变量)
互斥锁和条件变量 为了允许在线程或进程之间共享数据,同步时必须的,互斥锁和条件变量是同步的基本组成部分. 1.互斥锁 互斥锁是用来保护临界区资源,实际上保护的是临界区中被操纵的数据,互斥锁通常用于保护 ...
- 进程间通信机制(管道、信号、共享内存/信号量/消息队列)、线程间通信机制(互斥锁、条件变量、posix匿名信号量)
注:本分类下文章大多整理自<深入分析linux内核源代码>一书,另有参考其他一些资料如<linux内核完全剖析>.<linux c 编程一站式学习>等,只是为了更好 ...
- Linux互斥锁、条件变量和信号量
Linux互斥锁.条件变量和信号量 来自http://kongweile.iteye.com/blog/1155490 http://www.cnblogs.com/qingxia/archive/ ...
- node源码详解(七) —— 文件异步io、线程池【互斥锁、条件变量、管道、事件对象】
本作品采用知识共享署名 4.0 国际许可协议进行许可.转载保留声明头部与原文链接https://luzeshu.com/blog/nodesource7 本博客同步在https://cnodejs.o ...
- 非常精简的Linux线程池实现(一)——使用互斥锁和条件变量
线程池的含义跟它的名字一样,就是一个由许多线程组成的池子. 有了线程池,在程序中使用多线程变得简单.我们不用再自己去操心线程的创建.撤销.管理问题,有什么要消耗大量CPU时间的任务通通直接扔到线程池里 ...
- 互斥锁和条件变量(pthread)相关函数
互斥锁 #include <pthread.h> // 若成功返回0,出错返回正的Exxx值 // mptr通常被初始化为PTHREAD_MUTEX_INITIALIZER int pth ...
随机推荐
- 我对微服务、SpringCloud、k8s、Istio的一些杂想
一.微服务与SOA “微服务”是一个名词,没有这个名词之前也有“微服务”,一个朗朗上口的名词能让大家产生一个认知共识,这对推动一个事务的发展挺重要的,不然你叫微服务他叫小服务的大家很难集中到一个点上. ...
- linux初学者-Apache篇
linux初学者-Apache篇 Apache提供了超文本传输协议http,httpd是Apache超文本传输协议的主服务器.下文将对httpd的安装和配置进行简单的叙述. ...
- linux初学者-pxe装机篇
linux初学者-pxe装机篇 PXE的网络装机是客户机从自己的网卡启动,向本网络中的DHCP服务器索取ip,并从本网络的TFTP服务器中索取启动文件进行装机.此装机需要kickstart.tftp. ...
- python模块知识二 random -- 随机模块、序列化 、os模块、sys -- 系统模块
4.random -- 随机模块 a-z:97 ~ 122 A-Z :65 ~ 90 import random #浮点数 print(random.random())#0~1,不可指定 print( ...
- 记kepServer读写西门子PLC
在程序开发过程中为了测试方法或者验证某个属性的值是否正确 经常通过Kepserver 的 OPC Quick Client来手动置点或者读取点位 例如 这里显示的值都是经过转化后得到的十进制值,那我们 ...
- kube-scheduler源码分析
kubernetes集群三步安装 kube-scheduler源码分析 关于源码编译 我嫌弃官方提供的编译脚本太麻烦,所以用了更简单粗暴的方式编译k8s代码,当然官方脚本在编译所有项目或者夸平台编译以 ...
- luogu1220_关路灯 区间dp
传送门 区间dp f[i][j][state] : [i, j]区间 state=0 当前选i state = 1 当前选j 注意枚举的顺序 转移的设计时 在同时刻不在[i,j]区间里的数也要考虑 不 ...
- Netty学习(九)-Netty编解码技术之Marshalling
前面我们讲过protobuf的使用,主流的编解码框架其实还有很多种: ①JBoss的Marshalling包 ②google的Protobuf ③基于Protobuf的Kyro ④Apache的Thr ...
- if IE语句 | 判断浏览器IE版本及添加升级提示
本文引自:http://blog.csdn.net/u013372487/article/details/48521929 实现方法 判断当前浏览器是否IE6(或IE6内核) <!--[if I ...
- Netty基础系列(4) --堆外内存与零拷贝详解
前言 到目前为止,我们知道Nio当中有三个最最核心的组件,分别是:Selelctor,Channel,Buffer.在Netty基础系列(3) --彻底理解NIO 这一篇文章中只是进行了大致的介绍. ...
