C++ 并发编程之互斥锁和条件变量的性能比较

发布时间:2026/7/28 18:37:59
C++ 并发编程之互斥锁和条件变量的性能比较
介绍本文以最简单生产者消费者模型通过运行程序观察该进程的cpu使用率来对比使用互斥锁和互斥锁条件变量的性能比较。本例子的生产者消费者模型1个生产者5个消费者。生产者线程往队列里放入数据5个消费者线程从队列取数据取数据前需要判断一下队列中是否有数据这个队列是全局队列是线程间共享的数据所以需要使用互斥锁进行保护。即生产者在往队列里放入数据时其余消费者不能取反之亦然。互斥锁实现的代码#includeiostream// std::cout#includedeque// std::deque#includethread// std::thread#includechrono// std::chrono#includemutex// std::mutex// 全局队列std::dequeintg_deque;// 全局锁std::mutex g_mutex;// 生产者运行标记boolproducer_is_runningtrue;// 生产者线程函数voidProducer(){// 库存个数intcount8;do{// 智能锁初始化后即加锁保护的范围是代码花括号内花括号退出即会自动解锁// 可以手动解锁从而控制互斥锁的细粒度std::unique_lockstd::mutexlocker(g_mutex);// 入队一个数据g_deque.push_front(count);// 提前解锁缩小互斥锁的细粒度只针对共享的队列数据进行同步保护locker.unlock();std::cout生产者 我现在库存有 :countstd::endl;// 放慢生产者生产速度睡1秒std::this_thread::sleep_for(std::chrono::seconds(1));// 库存自减少count--;}while(count0);// 标记生产者打样了producer_is_runningfalse;std::cout生产者 我的库存没有了我要打样了std::endl;}// 消费者线程函数voidConsumer(intid){intdata0;do{std::unique_lockstd::mutexlocker(g_mutex);if(!g_deque.empty()){datag_deque.back();g_deque.pop_back();locker.unlock();std::cout消费者[id] : 我抢到货的编号是 :datastd::endl;}else{locker.unlock();}}while(producer_is_running);std::cout消费者[id] 卖家没有货打样了真可惜下次再来抢std::endl;}intmain(void){std::cout1 producer start ...std::endl;std::threadproducer(Producer);std::cout5 consumer start ...std::endl;std::thread consumer[5];for(inti0;i5;i){consumer[i]std::thread(Consumer,i1);}producer.join();for(inti0;i5;i){consumer[i].join();}std::coutAll threads joined.std::endl;return0;}互斥锁实现运行结果结果输出[rootlincoding condition]# g -stdc0x -pthread -D_GLIBCXX_USE_NANOSLEEP main.cpp -o main[rootlincoding condition]# ./main1 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[rootlincoding ~]# grep model name /proc/cpuinfo | wc -l4top命令查看cpu使用情况可见使用纯互斥锁cpu的开销是很大的main进程的cpu使用率达到了357.5%CPU系统开销的cpu为54.5%sy用户开销的cpu为18.2%us[rootlincoding ~]# toptop- 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带来的开销。// 消费者线程函数voidConsumer(intid){intdata0;do{std::unique_lockstd::mutexlocker(g_mutex);if(!g_deque.empty()){datag_deque.back();g_deque.pop_back();locker.unlock();std::cout消费者[id] : 我抢到货的编号是 :datastd::endl;}else{locker.unlock();// 当消费者没取到数据时就休息一下500毫秒std::this_thread::sleep_for(std::chrono::milliseconds(500));}}while(producer_is_running);std::cout消费者[id] 卖家没有货打样了真可惜下次再来抢std::endl;}从运行结果可知cpu使用率大大降低了[rootlincoding ~]# ps aux | grep -v grep |grep mainUSER 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()消费者线程使得消费者从队列取出一个数据。#includeiostream// std::cout#includedeque// std::deque#includethread// std::thread#includechrono// std::chrono#includemutex// std::mutex#includecondition_variable// std::condition_variable// 全局队列std::dequeintg_deque;// 全局锁std::mutex g_mutex;// 全局条件变量std::condition_variable g_cond;// 生产者运行标记boolproducer_is_runningtrue;// 生产者线程函数voidProducer(){// 库存个数intcount8;do{// 智能锁初始化后即加锁保护的范围是代码花括号内花括号退出即会自动解锁// 可以手动解锁从而控制互斥锁的细粒度std::unique_lockstd::mutexlocker(g_mutex);// 入队一个数据g_deque.push_front(count);// 提前解锁缩小互斥锁的细粒度只针对共享的队列数据进行同步保护locker.unlock();std::cout生产者 我现在库存有 :countstd::endl;// 唤醒一个线程g_cond.notify_one();// 睡1秒std::this_thread::sleep_for(std::chrono::seconds(1));// 库存自减少count--;}while(count0);// 标记生产者打样了producer_is_runningfalse;// 唤醒所有消费线程g_cond.notify_all();std::cout生产者 我的库存没有了我要打样了std::endl;}// 消费者线程函数voidConsumer(intid){// 购买的货品编号intdata0;do{// 智能锁初始化后即加锁保护的范围是代码花括号内花括号退出即会自动解锁// 可以手动解锁从而控制互斥锁的细粒度std::unique_lockstd::mutexlocker(g_mutex);// wait()函数会先调用互斥锁的unlock()函数然后再将自己睡眠在被唤醒后又会继续持有锁保护后面的队列操作// 必须使用unique_lock不能使用lock_guard因为lock_guard没有lock和unlock接口而unique_lock则都提供了g_cond.wait(locker);// 队列不为空if(!g_deque.empty()){// 取出队列里最后一个数据datag_deque.back();// 删除队列里最后一个数据g_deque.pop_back();// 提前解锁缩小互斥锁的细粒度只针对共享的队列数据进行同步保护locker.unlock();std::cout消费者[id] : 我抢到货的编号是 :datastd::endl;}// 队列为空else{locker.unlock();}}while(producer_is_running);std::cout消费者[id] 卖家没有货打样了真可惜下次再来抢std::endl;}intmain(void){std::cout1 producer start ...std::endl;std::threadproducer(Producer);std::cout5 consumer start ...std::endl;std::thread consumer[5];for(inti0;i5;i){consumer[i]std::thread(Consumer,i1);}producer.join();for(inti0;i5;i){consumer[i].join();}std::coutAll threads joined.std::endl;return0;}条件变量互斥锁运行结果[rootlincoding condition]# g -stdc0x -pthread -D_GLIBCXX_USE_NANOSLEEP main.cpp -o main[rootlincoding condition]#[rootlincoding condition]# ./main1 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开销非常的小[rootlincoding ~]# ps aux | grep -v grep |grep mainUSER 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的性能开销非常大可以使用互斥锁条件变量的方式当生产者线程生产了一个数据就唤醒消费者线程进行消费避免一些无用功的性能开销。