目录
1 前言
2 条件变量
3 unique_lock
为了更好的理解条件变量是什么,我们还是应当先思考一下为什么需要条件变量,还是先以一段程序为例:
using namespace std;
class MsgList //模拟消息的写入和读取
{
public:
void MsgWrite()
{
for (int i = 0; i < 10000; i++) //写入1000条消息,用i来模拟消息
{
lock_guard guard(m);
msgQue.push_back(i);
cout << "Write Message : " << i << endl;
}
}
void MsgRead()
{
while (true) //用死循环来循环判断消息列表中是否有消息,如果有的话就将其读出
{
if (!msgQue.empty())
{
lock_guard guard(m);
cout << "Read Message : " << msgQue.front() << endl; //读取最先来临的消息
msgQue.pop_front(); //将已读消息删除
}
else
{
lock_guard guard(m); //这里加锁是为了保证cout输出完整
cout << "There is no Message !" << endl;
}
}
}
private:
deque msgQue; //消息队列
mutex m; //互斥锁
};
int main()
{
MsgList myMsg;
thread ReadThread(&MsgList::MsgRead, &myMsg); //写线程
thread WriteThread(&MsgList::MsgWrite, &myMsg); //读线程
ReadThread.join();
WriteThread.join();
return 0;
}
这里通过互斥锁实现了msgQue的读和写,运行结果自然是没问题的,不过,这并非是最好的。为什么不好呢?不好就不好在读线程函数是个死循环,程序会一直循环判断消息队列是否有消息,这样就造成了CPU不必要的开销。
为什么会有这些不必要的开销呢?这是因为程序不知道到底消息队列中有没有消息,就一直需要去判断,如果能有一个好的办法,当消息队列中有消息的时候读线程再去执行,如果队列中没有消息的话就让读线程一直阻塞住。
那么,该如何实现呢?这就是条件变量的作用了。
在C++11中,条件变量通过condition_variable类来实现。先来看一下condition_variable的解释:
由此可知,条件变量实际上是condition_variable类的对象,通过这个对象可以实现对调用线程的阻塞。条件变量往往需要绑定一个unique_lock(也是互斥锁的一种实现),当在线程中通过条件变量调用wait函数时,该线程就会被阻塞住,直到在另一个线程中调用notify函数来唤醒这个线程。
对于其中的wait函数,它提供两种重载方式,如下所示:
这些描述有以下主要信息:
①wait函数会先对传入的unique_lock进行unlock,并且阻塞当前线程,unlock和block的操作,是一个原子行为。然后将当前线程添加到当前condition_variable对象的等待线程列表中;
②当唤醒线程调用notify_all或者notify_one时,就会解除wait线程的阻塞。解除阻塞之后,就会重新对unique_lock进行lock,然后wait函数返回;
③如果wait函数还传入了第二个参数pred,pred参数应当是个bool型的可调用对象。此时调用wait,会先判断第二个参数的返回值,如果返回false,就会执行第①步,否则直接返回;当被唤醒后再次判断第二个参数的返回值,如果返回false,会再次进入阻塞,否则直接返回;
④由于unique_lock的存在,保护了对可调用对象pred的访问。
⑤如果调用notify_all或者notify_one,实际上就是唤醒当前condition_variable对象的等待线程列表中的线程。
那么如何在上述程序中使用条件变量呢?如下所示:
class MsgList
{
public:
void MsgWrite()
{
for (int i = 0; i < 10000; i++)
{
unique_lock lck(m);
msgQue.push_back(i);
cout << "Write Message : " << i << endl;
mycond.notify_one(); //唤醒wait
}
}
void MsgRead()
{
while (true)
{
unique_lock lck(m);
mycond.wait(lck, [this]() { //调用wait函数,先解锁lck,然后判断lambda的返回值
return !msgQue.empty();
});
int ReadData = msgQue.front(); //执行到这里,说明msgQue非空,就可以读取数据了
msgQue.pop_front();
cout << "Read Message : " << ReadData << endl;
}
}
private:
deque msgQue;
mutex m;
condition_variable mycond;
};
在使用条件变量的时候,涉及到了unique_lock,现在说一下unique_lock是什么东西。
unique_lock与lock_guard相似,都能实现自动加锁和解锁,但是unique_lock比lock_guard使用更灵活,能实现更多的功能。unique_lock也是一个类,如上述代码中的unique_lock
与lock_guard不同的是,unique_lock可以进行临时解锁和再上锁,如在构造对象之后使用lck.unlock()就可以进行解锁,lck.lock()进行上锁,而不必等到析构时自动解锁。
除此之外,unique_lock还接受第二个参数来进行构造。两个参数构造的形式有以下几种:
unique_lock
unique_lock
unique_lock
unique_lock还有一些常用的成员函数:lck.lock():对lck加锁;
lck.unlock():对lck解锁;
lck.try_lock():尝试锁,如果锁成功则返回true,否则返回false;