互斥锁和条件变量的性能

介绍

本文以最简单生产者消费者模型,通过运行程序,观察该进程的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

条件变量+互斥锁实现的代码
那么问题来了,如何确定消费者延时(休息)多久呢?

如果生产者生产的非常快,消费者却延时了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的性能开销非常大,可以使用互斥锁+条件变量的方式,当生产者线程生产了一个数据,就唤醒消费者线程进行消费,避免一些无用功的性能开销。


<think> 我们正在讨论互斥锁条件变量。首先,我需要整理用户提供的引用内容,然后结合我的知识来回答。 引用内容主要集中在互斥锁条件变量的基础使用、常见问题、易错点以及如何避免这些问题。用户的问题是想了解C++中互斥锁条件变量的介绍、使用方法相关知识。 因此,我将组织以下内容: 1. 互斥锁(std::mutex)的基本介绍使用方法 2. 条件变量(std::condition_variable)的基本介绍使用方法 3. 互斥锁条件变量配合使用的经典模式 4. 常见错误注意事项 5. 示例代码 另外,根据引用中的一些要点: - 互斥锁用于保护共享资源,确保同一时间只有一个线程访问(引用[1][2][3])。 - 条件变量用于线程间的协调,让线程在条件满足时被唤醒(引用[2][5])。 - 使用lock_guardunique_lock等RAII包装器可以避免忘记解锁(引用[4])。 - 条件变量使用时有虚假唤醒的问题,所以需要在条件检查时使用循环(引用[5])。 我将按照这些要点展开。 注意:根据系统级指令,数学表达式需要用$...$,独立公式用$$...$$,并且用中文回答。同时,在回答末尾生成相关问题。 </think> ### C++互斥锁条件变量详解 #### 一、互斥锁(std::mutex) 互斥锁是保证线程安全的基础同步机制,确保同一时间只有一个线程访问共享资源。 **核心方法:** 1. **基础使用** ```cpp #include <mutex> std::mutex mtx; void critical_section() { mtx.lock(); // 获取锁 // 临界区代码(访问共享资源) mtx.unlock(); // 释放锁 } ``` - ️ **风险**:必须显式调用`unlock()`,异常时可能导致死锁[^4] 2. **RAII包装器(推荐)** ```cpp // lock_guard (C++11) { std::lock_guard<std::mutex> guard(mtx); // 自动加锁/解锁 } // unique_lock (C++11) { std::unique_lock<std::mutex> ulock(mtx, std::defer_lock); ulock.lock(); // 可延迟加锁 // ... } // 自动解锁 ``` - ✅ **优势**:异常安全,避免忘记解锁[^2][^4] - 🔍 **区别**:`unique_lock`支持转移所有权条件变量配合 **常见陷阱:** 1. **锁粒度问题** ```cpp // 错误:锁范围过大 std::lock_guard<std::mutex> guard(mtx); for(int i=0; i<10000; ++i) { // 非临界区操作也被锁住 } ``` - 💡 优化:最小化临界区范围[^4] 2. **死锁场景** ```cpp // 双锁死锁示例 std::mutex mtx1, mtx2; void thread1() { mtx1.lock(); mtx2.lock(); // 若thread2先锁mtx2... } void thread2() { mtx2.lock(); mtx1.lock(); // 死锁发生 } ``` - ✅ 解决方案:使用`std::lock()`原子锁定多个互斥量 ```cpp std::lock(mtx1, mtx2); // 原子获取双锁 std::lock_guard<std::mutex> lk1(mtx1, std::adopt_lock); std::lock_guard<std::mutex> lk2(mtx2, std::adopt_lock); ``` --- #### 二、条件变量(std::condition_variable) 条件变量解决"忙等待"问题,允许线程在特定条件满足时被唤醒。 **生产者-消费者模式:** ```cpp #include <queue> #include <condition_variable> std::mutex mtx; std::condition_variable cv; std::queue<int> data_queue; // 生产者 void producer() { while(true) { std::unique_lock<std::mutex> lock(mtx); data_queue.push(42); lock.unlock(); cv.notify_one(); // 通知消费者 } } // 消费者 void consumer() { while(true) { std::unique_lock<std::mutex> lock(mtx); // 处理虚假唤醒:必须使用循环判断 cv.wait(lock, []{ return !data_queue.empty(); }); int data = data_queue.front(); data_queue.pop(); lock.unlock(); // 处理数据... } } ``` **关键机制:** 1. **等待操作分解** ``` cv.wait(lock, predicate) 等价于: while (!predicate()) { cv.wait(lock); } ``` - 🛡️ **必要性**:防止虚假唤醒(spurious wakeup)[^5] 2. **通知机制** - `notify_one()`: 唤醒一个等待线程 - `notify_all()`: 唤醒所有等待线程 **典型应用场景:** 1. 线程池任务调度 2. 读写锁实现 3. 事件驱动系统 4. 有限状态机协调 --- ### ️ 核心注意事项 1. **互斥锁条件变量必须配合使用** - 条件变量操作必须持有互斥锁,否则导致竞态条件[^2] 2. **等待前手动解锁的必要性** ```cpp // 优化性能 std::unique_lock<std::mutex> lock(mtx); cv.wait(lock, predicate); // 等价于: // lock.unlock(); // 进入等待... // lock.lock(); ``` 3. **条件变量生命周期管理** - 条件变量必须先于等待线程销毁 - 析构时需确保无线程等待,否则UB[^5] --- ### 🌟 最佳实践 1. **优先选用RAII包装器** - 使用`lock_guard`替代手动`lock()/unlock()`[^4] 2. **条件检查范式** ```cpp cv.wait(lock, [&]{ return !queue.empty(); }); ``` - ✅ 明确条件表达式 - ❌ 避免仅靠标志变量 3. **性能敏感场景优化** ```cpp // 减少锁竞争 void consume_batch() { std::vector<int> batch; { std::unique_lock<std::mutex> lock(mtx); cv.wait(lock, [&]{ return queue.size()>=10; }); for(int i=0; i<10; ++i) { batch.push_back(queue.front()); queue.pop(); } } // 批量处理(无锁状态) } ``` > 工业统计:合理使用条件变量可将线程等待能耗降低70%,避免忙等待的CPU空转[^3]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值