10C++11多线程编程之条件变量std:: condition_variable、wait()、notify_one()、notify_all()

1 为何引入条件变量

解决while不断循环收发消息,让它只有消息到来时才进行处理。大大减少CPU的使用率和提高程序效率。

2 条件变量std:: condition_variable
std:: condition_variable实际上是个类,是一个与条件相关的类,说白了就是等待一个条件的达成。这个类是需要和互斥量来配合工作的,使用时定义一个该类对象即可。
实例代码:
线程A:循环等待一个条件满足,但若条件不满足会休眠在条件变量,并不会占用CPU。
线程B:专门往消息队列扔消息(数据),然后通知其它线程。

3 wait()函数详解

//1 调用wait之前必须上锁
//2 wait的作用有三个:1)阻塞;2)解锁;3)上锁
/*
	以下将详细说明
	1 首先先说明最重要的一点:
	1.1)每次notify后,条件变量不再阻塞,此时必定先上锁再判断参数2。拿不到锁就先阻塞在锁上,并不断尝试拿锁。
	假设先判断参数2再上锁,那么当多个线程(假设10个)都满足不为空时,
	这10个线程都阻塞等待获取锁,而此时队列只有2个元素,当某两个线程先获取到锁消耗完后,
	其余8个线程依次获取到锁,但是却无法取到元素,程序必然出现问题。
	故每次notify后,条件变量不再阻塞,此时必定先上锁再判断参数2。

	2 依次讲解有无参数2的情况
	2.1有参数2的情况):
	若参数2返回true即队列不为空,wait直接返回处理共享代码。
	若参数2返回false,wait将解锁并继续阻塞条件变量,等待下一次的notify。
	2.2无参数2的情况):
	一旦有notify,wait就直接返回。这是非常危险的,当队列没数据但仍被唤醒的话(虚假唤醒),程序将出现崩溃。(已测试)
*/

3.1正确的利用条件变量配合互斥量使用案例

#include<iostream>
#include<thread>
#include<string>
#include<vector>
#include<list>
#include<mutex>

using namespace std;

//准备用成员函数作为线程函数的方法写线程,成为消息处理类
class A {
public:

	//把收到的消息入到一个队列的线程
	void inMsgRecvQueue() {
		for (int i = 0; i < 10000; i++) {
			cout << "inMsgRecvQueue()执行,插入一个元素" << i << endl;
			unique_lock<mutex> uLock(my_mutex);
			msgRecvQueue.push_back(i);
			my_cond.notify_one();
		}
	}

	//舍弃原来不断循环去消息的函数,换成下面的
	void outMsgRecvQueue(int &command) {
		while (1){
			unique_lock<mutex> uLock(my_mutex);
			//1 调用wait之前必须上锁
			//2 wait的作用有三个:1)阻塞;2)解锁;3)上锁
			/*
				以下将详细说明
				1 首先先说明最重要的一点:
				1.1)每次notify后,条件变量不再阻塞,此时必定先上锁再判断参数2。拿不到锁就先阻塞在锁上,并不断尝试拿锁。
				假设先判断参数2再上锁,那么当多个线程(假设10个)都满足不为空时,
				这10个线程都阻塞等待获取锁,而此时队列只有2个元素,当某两个线程先获取到锁消耗完后,
				其余8个线程依次获取到锁,但是却无法取到元素,程序必然出现问题。
				故每次notify后,条件变量不再阻塞,此时必定先上锁再判断参数2。

				2 依次讲解有无参数2的情况
				2.1有参数2的情况):
				若参数2返回true即队列不为空,wait直接返回处理共享代码。
				若参数2返回false,wait将解锁并继续阻塞条件变量,等待下一次的notify。
				2.2无参数2的情况):
				一旦有notify,wait就直接返回。这是非常危险的,当队列没数据但仍被唤醒的话(虚假唤醒),程序将出现崩溃。(已测试)
			*/

			my_cond.wait(uLock, [this]() {
				if (!msgRecvQueue.empty()) {
					return true;
				}
				return false;
			});

			command = msgRecvQueue.front();
			msgRecvQueue.pop_front();
			cout << "outMsgRecvQueue()执行,取出一个元素" << command << endl;

			//可以下面处理消息,也可以通过command传出外部处理消息
			//uLock.unlock();//可以提前解锁处理其它
		}
		
	}

private:
	std::list<int> msgRecvQueue;//容器(消息队列),代表玩家发送过来的命令。
	std::mutex my_mutex;
	std::condition_variable my_cond;
};

int main(){

	A myobja;
	int command = 0;//取出的命令

	std::thread myOutMsgObj(&A::outMsgRecvQueue, &myobja, std::ref(command));//第二个参数,地址,才能保证线程里用的是同一个对象
	std::thread myInMsgObj(&A::inMsgRecvQueue, &myobja);

	myOutMsgObj.join();
	myInMsgObj.join();

	cout << "主线程执行!" << endl;

	return 0;
}

结果可以看到,眼看都能看到执行的速度比之前只用互斥量快,并且代码稳定。
在这里插入图片描述

3.2错误的使用wait案例–>不能缺省参数2
在使用wait搭配互斥量使用时,万万不能缺省参数2的判断,否则当数据没有插入,但它仍然notify唤醒你(虚假唤醒),wait返回,容器list为空你pop就会出现程序崩溃。
测试的错误代码:

#include<iostream>
#include<thread>
#include<string>
#include<vector>
#include<list>
#include<mutex>

using namespace std;

//准备用成员函数作为线程函数的方法写线程,成为消息处理类
class A {
public:

	//把收到的消息入到一个队列的线程
	void inMsgRecvQueue() {
		for (int i = 0; i < 10000; i++) {
			cout << "inMsgRecvQueue()执行,插入一个元素" << i << endl;
			unique_lock<mutex> uLock(my_mutex);
			//msgRecvQueue.push_back(i);//测试wait无参数2的虚假唤醒
			my_cond.notify_one();
		}
	}

	//舍弃原来不断循环去消息的函数,换成下面的
	void outMsgRecvQueue(int &command) {
		while (1){
			unique_lock<mutex> uLock(my_mutex);
			my_cond.wait(uLock);//error,必须加参数2判断,否则出现崩溃

			command = msgRecvQueue.front();
			msgRecvQueue.pop_front();
			cout << "outMsgRecvQueue()执行,取出一个元素" << command << endl;


			//可以下面处理消息,也可以通过command传出外部处理消息
		}
		
	}

private:
	std::list<int> msgRecvQueue;//容器(消息队列),代表玩家发送过来的命令。
	std::mutex my_mutex;
	std::condition_variable my_cond;
};

int main(){

	A myobja;
	int command = 0;//取出的命令

	std::thread myOutMsgObj(&A::outMsgRecvQueue, &myobja, std::ref(command));//第二个参数,地址,才能保证线程里用的是同一个对象
	std::thread myInMsgObj(&A::inMsgRecvQueue, &myobja);

	myOutMsgObj.join();
	myInMsgObj.join();

	cout << "主线程执行!" << endl;

	return 0;
}


结果可以看到,程序出现崩溃。
在这里插入图片描述

4 上述代码思考

上面的代码可以说是安全效率也不错,但不能说是完美的,仍存在以下问题。

  • 1)由于inMsg是不断for循环push数据,每当锁释放后可能重新拿到锁,拿锁的能力比outMsg强,这就导致输入队列的消息多,输出的线程处理不过来,例如玩家消息过多,服务器处理不过去,那么你将丢失非常多的玩家。解决思想是开多个线程或者使用线程池处理。
  • 2)由于输入消息逻辑简单,而outMsg在拿到消息后,一般需要继续往下处理,那么此时再notify的话,outMsg不处于wait,所以这次notify就相当于浪费了,这个也是消息处理不及时的原因。例如玩家发消息想抽卡,但是服务器在做其它事情,那么玩家只能等待了,如果等久了玩家肯定火冒三丈,这也是非常严重的。同样可以开多个线程或者直接线程池处理。
  • 3)所以notify不一样每次都能唤醒其它线程工作,必须有足够的空闲线程才行。
  • 4)处理消息时,尽量将消息传出处理。
  • 5)在使用这些函数运用到商业时,必须进行深入思考。

5 notify_all()

1)notify_one()每次只能通知一个空闲线程。例子看上面即可。

  • 2)notify_all()通知多个(所有)空闲的线程。那么多个空闲线程将不再阻塞条件变量,但是会阻塞在互斥锁上,没拿到锁的线程都会不断尝试获取锁,所以使用notify_all时效率主要消耗在竞争锁上。故我们使用notify_all的场所是:保持有多个空闲的线程,锁要尽快的释放,让竞争锁的线程尽快拿到锁,并且想办法将正在处理的线程变成空闲去等待notify准备下一步。这就是我们notify_all使用的场所和效率优化思想。—但是我将下面两个空闲线程换成5-10个变化也不大,有空再想想解决办法。
  • 3)所以我们下面的代码notify_one与notify_all的结果差不多,以为即使通知了多个线程,但是由于没拿到锁的线程,仍会阻塞,只不过阻塞的对象不同,原来是条件变量,变成阻塞在锁上。

看下面测试代码。
测试notify_all()通知多个线程代码案例。

#include<iostream>
#include<thread>
#include<string>
#include<vector>
#include<list>
#include<mutex>

using namespace std;

//准备用成员函数作为线程函数的方法写线程,成为消息处理类
class A {
public:

	//把收到的消息入到一个队列的线程
	void inMsgRecvQueue() {
		for (int i = 0; i < 10000; i++) {
			unique_lock<mutex> uLock(my_mutex);
			cout << "inMsgRecvQueue()执行,插入一个元素" << i << endl;//将打印放在锁内,方便观察唤醒某个线程
			msgRecvQueue.push_back(i);
			//my_cond.notify_one();
			my_cond.notify_all();
		}
	}

	//舍弃原来不断循环去消息的函数,换成下面的
	void outMsgRecvQueue(int &command) {
		while (1){
			unique_lock<mutex> uLock(my_mutex);
			//1 调用wait之前必须上锁
			//2 wait的作用有三个:1)阻塞;2)解锁;3)上锁
			/*
				以下将详细说明
				1 首先先说明最重要的一点:
				1.1)每次notify后,条件变量不再阻塞,此时必定先上锁再判断参数2。拿不到锁就先阻塞在锁上,并不断尝试拿锁。
				假设先判断参数2再上锁,那么当多个线程(假设10个)都满足不为空时,
				这10个线程都阻塞等待获取锁,而此时队列只有2个元素,当某两个线程先获取到锁消耗完后,
				其余8个线程依次获取到锁,但是却无法取到元素,程序必然出现问题。
				故每次notify后,条件变量不再阻塞,此时必定先上锁再判断参数2。

				2 依次讲解有无参数2的情况
				2.1有参数2的情况):
				若参数2返回true即队列不为空,wait直接返回处理共享代码。
				若参数2返回false,wait将解锁并继续阻塞条件变量,等待下一次的notify。
				2.2无参数2的情况):
				一旦有notify,wait就直接返回。这是非常危险的,当队列没数据但仍被唤醒的话(虚假唤醒),程序将出现崩溃。(已测试)
			*/

			my_cond.wait(uLock, [this]() {
				if (!msgRecvQueue.empty()) {
					return true;
				}
				return false;
			});

			command = msgRecvQueue.front();
			msgRecvQueue.pop_front();
			cout << "outMsgRecvQueue()执行,取出一个元素" << command << "tid=" << std::this_thread::get_id() << endl;
			
			//锁要尽量的快释放,让竞争锁的线程尽快拿到锁,并且想办法将正在处理的线程变成空闲去等待notify准备下一步
			uLock.unlock();

			//可以下面处理消息,也可以通过command传出外部处理消息
		}
		
	}

private:
	std::list<int> msgRecvQueue;//容器(消息队列),代表玩家发送过来的命令。
	std::mutex my_mutex;
	std::condition_variable my_cond;
};

int main(){

	A myobja;
	int command1 = 0;//取出的命令
	int command2 = 0;

	std::thread myOutMsgObj(&A::outMsgRecvQueue, &myobja, std::ref(command1));//第二个参数,地址,才能保证线程里用的是同一个对象
	std::thread myOutMsgObj2(&A::outMsgRecvQueue, &myobja, std::ref(command2));
	std::thread myInMsgObj(&A::inMsgRecvQueue, &myobja);

	myOutMsgObj.join();
	myInMsgObj.join();

	cout << "主线程执行!" << endl;

	return 0;
}

上述代码换成notify_one或者notify_all结果都和下图差不多。
在这里插入图片描述

  • 1
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值