条件变量condition_variable
condition_variable是一个和条件相关的类,本质上就是等待一个条件达成。使用的时候必须和互斥量mutex配合使用。
使用场景:增加效率
//把消息从消息队列取出
void outMsgRecvQueue() {
int command = 0; //指令为command;
for (int i = 0; i < 10000; i++) {
bool result = outMsgLULProc(command); //将所有对共享数据的访问都封装成一个函数,方便加锁
if (result) {
//消息队列不为空
//对根据命令对数据进行处理
}
else {
//消息队列为空
cout << "消息队列为空!" << endl;
}
}
}
bool outMsgLULProc(int &command) {
//对共享资源上锁
unique_lock<mutex> myUnique2(m_mutex2);
if (!msgRecvQueue.empty()) {
//消息不为空
int command = msgRecvQueue.front(); //返回第一个元素,但不检查是否存在
msgRecvQueue.pop_front();
return true;
}
return false;
}
上面是(五)中写的取消息队列的部分代码,outMsgRecvQueue
中不停的循环,调用outMsgLULProc
查看队列是否为空,只有消息不为空,才会进行操作,CPU被极大的浪费在了查看队列是否为空上;(六)中提到的双重加锁,可以一定程序上减少判断,提高效率,这里利用条件变量可以更加提高效率:
一、outMsgRecvQueue调用wait()
outMsgLULProc
修改成一直等待,消息队列中有数据的时候,来通知线程,线程再来取数据操作,这样就避免了需要CPU把资源浪费到一直判断消息是否为空上。
//把消息从消息队列取出
void outMsgRecvQueue() {
int command = 0;
while (true) {
unique_lock<mutex> myUnique(m_mutex2);
m_cond.wait(myUnique, [this] { //一个lamda表达式就是一个可调用对象(函数)
if (!msgRecvQueue.empty()) {
return true;
}
else
{
return false;
}
});
//未完,待续
}
}
//...
condition_variable m_cond; //生成一个条件变量对象
wait()方法是用来等待一个条件
-
如果第二个参数的lambda表达式返回值是false,那么wait()将解锁互斥量,并阻塞到本行
-
如果第二个参数的lambda表达式返回值是true,那么wait()直接返回并继续执行。
-
阻塞的时机为,一直阻塞到**其他某个线程调用notify_one()**成员函数为止;
-
如果没有第二个参数,那么效果跟第二个参数lambda表达式返回false效果一样
二、inMsgRecvQueue调用notice_one()
继续修改inMsgRecvQueue
,增加调用notify_one()成员函数:
//把收到的消息加入消息队列
void inMsgRecvQueue() {
for (int i = 0; i < 10000; i++) {
cout << "插入一个元素到消息队列";
//互斥量mutex的使用,保护共享资源
unique_lock<mutex> myUnique1(m_mutex1);
msgRecvQueue.push_back(i);
m_cond.notify_one(); //去把wait的线程唤醒,执行完这一行,outMsgRecvQueue中的wait会被唤醒
//唤醒之后继续他的操作
//其它处理步骤...
}
}
三、outMsgRecvQueue的wait被notice_one唤醒后
-
wait()不断尝试获取互斥量锁,如果获取不到那么流程就卡在wait()这里等待获取,如果获取到了,那么wait()就继续执行,这也表明获取到了锁。(也就是说,只要执行到了wait后面的代码,此线程一定获取到了锁)
-
如果wait有第二个参数就判断这个lambda表达式。
2.1 如果表达式为false,那wait又对互斥量解锁,然后又休眠,等待再次被notify_one()唤醒
2.2 如果lambda表达式为true,则wait返回,流程可以继续执行(此时互斥量已被锁住)。
- 如果wait没有第二个参数,则wait返回,流程走下去。
完整代码:
class MyPrint {
public:
//把收到的消息加入消息队列
void inMsgRecvQueue() {
for (int i = 0; i < 10000; i++) {
cout << "插入一个元素到消息队列";
//互斥量mutex的使用,保护共享资源
unique_lock<mutex> myUnique1(m_mutex1);
msgRecvQueue.push_back(i);
m_cond.notify_one(); //去把wait的线程唤醒,执行完这一行,outMsgRecvQueue中的wait会被唤醒
//唤醒之后继续他的操作
//其它处理步骤...
}
}
//把消息从消息队列取出
void outMsgRecvQueue() {
int command = 0;
while (true) {
unique_lock<mutex> myUnique(m_mutex2);
//当其它线程用notice_one将wait唤醒
m_cond.wait(myUnique, [this] { //一个lamda表达式就是一个可调用对象(函数)
if (!msgRecvQueue.empty()) {
return true;
}
else
{
return false;
}
});
//wait后,队列一定不为空,一定获取到了锁
command = msgRecvQueue.front(); //返回第一个元素,但不检查是否存在
msgRecvQueue.pop_front();
}
}
list<int> msgRecvQueue; //消息队列
//死锁演示
mutex m_mutex1;
mutex m_mutex2;
condition_variable m_cond; //生成一个条件变量对象
};
PS:重点是理解wait被notice_one唤醒之后的流程。
深入思考
上面的代码可能导致出现一种情况:
事实上执行的时候,不一定是inMsgRecvQueue()
和outMsgRecvQueue()
你执行一次,我执行一次,这么简单,两个进程获取锁,都是概率成功的,竞争锁的时候,谁获得和调度算法有关,都是可能成功的。
因为outMsgRecvQueue()
与inMsgRecvQueue()
并不是一对一执行的,所以当程序循环执行很多次以后,可能在消息队列中已经有了很多消息,但是,outMsgRecvQueue
还是被唤醒一次只处理一条数据。这时可以考虑把outMsgRecvQueue
多执行几次,或者对inMsgRecvQueue
进行限流。
notice_all()
notify_one()
:通知一个线程的wait()
notify_all()
:通知所有线程的wait()