介绍

本文以最简单生产者消费者模型,通过运行程序,观察该进程的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++ 并发编程之互斥锁和条件变量的性能比较的更多相关文章

  1. 【Linux C 多线程编程】互斥锁与条件变量

    一.互斥锁 互斥量从本质上说就是一把锁, 提供对共享资源的保护访问. 1) 初始化: 在Linux下, 线程的互斥量数据类型是pthread_mutex_t. 在使用前, 要对它进行初始化: 对于静态 ...

  2. python 并发编程 多进程 互斥锁 目录

    python 并发编程 多进程 互斥锁 模拟抢票 互斥锁与join区别

  3. linux c 线程间同步(通信)的几种方法--互斥锁,条件变量,信号量,读写锁

    Linux下提供了多种方式来处理线程同步,最常用的是互斥锁.条件变量.信号量和读写锁. 下面是思维导图:  一.互斥锁(mutex)  锁机制是同一时刻只允许一个线程执行一个关键部分的代码. 1 . ...

  4. linux 线程的同步 二 (互斥锁和条件变量)

    互斥锁和条件变量 为了允许在线程或进程之间共享数据,同步时必须的,互斥锁和条件变量是同步的基本组成部分. 1.互斥锁 互斥锁是用来保护临界区资源,实际上保护的是临界区中被操纵的数据,互斥锁通常用于保护 ...

  5. 进程间通信机制(管道、信号、共享内存/信号量/消息队列)、线程间通信机制(互斥锁、条件变量、posix匿名信号量)

    注:本分类下文章大多整理自<深入分析linux内核源代码>一书,另有参考其他一些资料如<linux内核完全剖析>.<linux c 编程一站式学习>等,只是为了更好 ...

  6. Linux互斥锁、条件变量和信号量

    Linux互斥锁.条件变量和信号量  来自http://kongweile.iteye.com/blog/1155490 http://www.cnblogs.com/qingxia/archive/ ...

  7. node源码详解(七) —— 文件异步io、线程池【互斥锁、条件变量、管道、事件对象】

    本作品采用知识共享署名 4.0 国际许可协议进行许可.转载保留声明头部与原文链接https://luzeshu.com/blog/nodesource7 本博客同步在https://cnodejs.o ...

  8. 非常精简的Linux线程池实现(一)——使用互斥锁和条件变量

    线程池的含义跟它的名字一样,就是一个由许多线程组成的池子. 有了线程池,在程序中使用多线程变得简单.我们不用再自己去操心线程的创建.撤销.管理问题,有什么要消耗大量CPU时间的任务通通直接扔到线程池里 ...

  9. 互斥锁和条件变量(pthread)相关函数

    互斥锁 #include <pthread.h> // 若成功返回0,出错返回正的Exxx值 // mptr通常被初始化为PTHREAD_MUTEX_INITIALIZER int pth ...

随机推荐

  1. Webpack打包效率优化篇

    Webpack基础配置: 语法解析:babel-loader 样式解析:style-loader css解析:css-loader less解析:less-loader 文件解析:url-loader ...

  2. 转 java - java基础知识点

    转 https://www.cnblogs.com/xdp-gacl/p/3641769.html 1.一个".java"源文件中是否可以包括多个类(不是内部类)?有什么限制? 可 ...

  3. myeclipse中更改默认jdk版本出错( Target is not a JDK root. System library was not found)

    原因是我的本地jdk版本是9.0,将jdk版本更改至8.0即可导入成功. jdk9.0导入myeclipse中去会有此类问题的发生,因此没有必要使用最新的jdk版本.

  4. activeMQ_helloworld(一)

    一.activeMQ下载,直接在Linux上wget http://mirror.bit.edu.cn/apache//activemq/5.14.5/apache-activemq-5.14.5-b ...

  5. thinkphp 插件

    1.切换到项目根目录,使用composer require 5ini99/think-addons:dev-master命令安装thinkphp插件 如果是root用户或是管理员执行的话会有提示 等一 ...

  6. 关于Hibernate查询对象调用set方法自动同步到数据库解决方案

    Hibernate的get和load方法查询出的实体都是持久化对象,拿到该对象后,如果你调用了该对象的set方法,如果再同一个事务里面,那么在事务递交的时候,Hibernate会把你设置的值自动更新到 ...

  7. js 实现 联动

    使用jQuery实现联动效果 应用场景:收货地址 1.准备三个下拉框 <select class="changeArea" id='province'> <opt ...

  8. 今天代码中接触到了一个新的东西。js的上下自动滚动,无缝对接。

    js的上下自动滚动,无缝对接.为什么会用到这个东西呢?因为我在做公司的官网项目的修改的时候.有一个产品介绍的页面,会有很多的产品出现在,中间部分的列表里.但是又不能够使用分页.所以我就在想如果,列表数 ...

  9. C语言数组排序——冒泡排序、选择排序、插入排序

    一.冒泡排序 原理解析:(以从小到大排序为例)在一排数字中,将第一个与第二个比较大小,如果后面的数比前面的小,则交换他们的位置. 然后比较第二.第三个……直到比较第n-1个和第n个,此时,每一次比较都 ...

  10. 详解阿里P7架构师是怎么在Spring中实现事务暂停

    摘要 Spring框架是一个流行的基于轻量级控制反转容器的Java/J2EE应用框架,尤其在数据访问和事务管理方面的能力是众所周知的.Spring的声明性事务分离可以应用到任何POJO目标对象,并且包 ...