📕条件变量
与本文无关的知识联系:
一、call_once
- 函数模板,第一个参数为标记,第二个参数为要调用的函数名,如test()
- 功能:保证写入第二个参数的函数(如test() )只能被调用一次。具备互斥量的能力,但互斥量消耗的资源少,更高效
- call_once(), 第一个参数的标记为:
std::once_flag
,实际上once_flag是一个结构体,记录函数是否已调用过。
例子:
//用once_flag实现单例模式
std::once_flag flag;
class Singleton
{
public:
static void CreateInstance()//call_once保证其只被调用一次
{
instance = new Singleton;
}
//两个线程同时执行到这里,其中一个线程要等另外一个线程执行完毕
static Singleton * getInstance() {
call_once(flag, CreateInstance);
return instance;
}
private:
Singleton() {}
static Singleton *instance;
};
Singleton * Singleton::instance = NULL;
本文正题开始:
二、condition_variable
- 实际上是一个类,是一个和条件相关的类,说白了就是等待一个条件达成!这个类需要和互斥量配合工作,用的时候我们要生成这个类对象,下面都是该类中的方法。
2.1 wait()
先看例子:
std::mutex myMutex;
std::unique_lock<std::mutex> uniLock(myMutex);
std::condition_variable cv;
//带两个参数
cv.wait(uniLock, [this] {
if (!msgRecvQueue.empty()) return true;
return false;
});
//只有一个参数
cv.wait(uniLock);
- wait()用来等待一个东西,第一个参数是要操作的锁,第二个参数是函数对象(不懂函数对象可以去百度理解,我们后面举例中都采用lambda表达式)
- 功能:①如果第二个参数的lambda表达式返回值是false,那么wait()将解锁互斥量,并阻塞到本行;②如果第二个参数的lambda表达式返回值是true,那么wait()直接返回并继续执行。
- 如果没有第二个参数,那么效果跟第二个参数lambda表达式返回false效果一样,wait()将解锁互斥量,并阻塞到本行。
- 阻塞到其他某个线程调用notify_one()成员函数为止。
当其他线程用notify_one()或者notify_all() 将本线程wait()唤醒后,这个wait恢复后:
1、wait()不断尝试获取互斥量锁,如果获取不到那么流程就卡在wait()这里等待获取继续获取锁,如果获取到了,那么wait()就继续执行2
2.1、如果wait有第二个参数就判断这个lambda表达式。
a)如果表达式为false,那wait又对互斥量解锁,然后又休眠,等待再次被notify_one()唤醒
b)如果lambda表达式为true,则wait返回,流程可以继续执行(此时互斥量已被锁住)。
2.2、如果wait没有第二个参数,则wait返回,流程走下去。
2.2 wait_for()
std::condition_variable::wait_for的原型有两种:
//第一种不带pre的
template <class Rep, class Period>
cv_status wait_for (unique_lock<mutex>& lck,
const chrono::duration<Rep,Period>& rel_time);
//第二种,带有pred
template <class Rep, class Period, class Predicate>
bool wait_for (unique_lock<mutex>& lck,
const chrono::duration<Rep,Period>& rel_time, Predicate pred);
即我们可以写三个参数,我们看看带谓词pre的版本(pre其实也就是wait的第二个参数),wait_for会阻塞其所在线程(该线程应当拥有lock),直至超过了rel_time,或者谓词返回true。在阻塞的同时会自动调用lck.unlock()
让出线程控制权。对上述行为进行总结:
- 只要谓词返回true(阻塞期间只要notify了才会看谓词状态),立刻唤醒线程(返回值为true);
- 当谓词为false,没超时就继续阻塞,超时了就唤醒(此时返回值为false);
- 当其他线程使用
notify_one
或者notify_all
进行唤醒时,取决于谓词的状态,若为false,则为虚假唤起,线程依然阻塞。
2.3 notify_one()
- 只唤醒等待队列中的第一个线程,不存在锁争用(同队列中不存在),所以能够立即获得锁。其余的线程不会被唤醒,需要等待再次调用
notify_one()
或者notify_all()
。
2.4 notify_all()
会唤醒所有等待队列中阻塞的线程,存在锁争用,只有一个线程能够获得锁,而其余的会接着尝试获得锁(类似轮询)
tips: ,java必须在锁内(与wait线程一样的锁)调用notify。但c++是不需要上锁调用的,如果在锁里调用,可能会导致被立刻唤醒的线程继续阻塞(因为锁被notify线程持有)。c++标准在通知调用中,直接将等待线程从条件变量队列转移到互斥队列,而不唤醒它,来避免此"hurry up and wait"场景。我在做多线程的题目的时候,notify_one 是在锁内末尾的时候调用的。
代码的优化:
参考一下我的笔记中:拿出数据的函数:
第一种写法:
//拿取数据的函数
bool outMsgPro(int& command) {
{
std::lock_guard<std::mutex> myGuard(myMutex);
if (!msgRecvQueue.empty()) {//非空就进行操作
command = msgRecvQueue.front();
msgRecvQueue.pop();
return true;
}
}
//其他操作代码
return false;
}
不管信息接收队列(msgRecvQueue)是不是空,都要加锁解锁,大大降低了效率------这个问题在设计模式中单例模式也有说明
进一步优化:第二种写法(正常优化写法)
//拿取数据的函数
bool outMsgPro(int& command) {
//双重锁定
if (!msgRecvQueue.empty()) {
std::lock_guard<std::mutex> myGuard(myMutex);
if (!msgRecvQueue.empty()) {//非空就进行操作
command = msgRecvQueue.front();
msgRecvQueue.pop();
return true;
}
}
//其他操作代码
return false;
}
再进一步优化写法:第三种写法
//拿取数据的函数
std::condition_variable cv;
void outMsgPro(int& command) {
std::lock_guard<std::mutex> myGuard(myMutex);
//采用wait方式,拿到数据
cv.wait(myGuard, [this] {
if (!msgRecvQueue.empty()) return true;
return false;
});
command = msgRecvQueue.front();
msgRecvQueue.pop();
myGuard.unlock();
//其他操作代码
return;
}