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结果都和下图差不多。