多线程06:条件变量


📕条件变量

与本文无关的知识联系:

一、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; } 
发表评论

相关文章