• 多线程06:条件变量


    📕条件变量

    与本文无关的知识联系:

    一、call_once#

    • 函数模板,第一个参数为标记,第二个参数为要调用的函数名,如test()
    • 功能:保证写入第二个参数的函数(如test() )只能被调用一次。具备互斥量的能力,但互斥量消耗的资源少,更高效
    • call_once(), 第一个参数的标记为:std::once_flag,实际上once_flag是一个结构体,记录函数是否已调用过。

    例子:

    Copy
    //用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()#

    先看例子:

    Copy
    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的原型有两种:

    Copy
    //第一种不带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 是在锁内末尾的时候调用的。

    代码的优化:

    参考一下我的笔记中:拿出数据的函数:

    第一种写法:

    Copy
    //拿取数据的函数 bool outMsgPro(int& command) { { std::lock_guard<std::mutex> myGuard(myMutex); if (!msgRecvQueue.empty()) {//非空就进行操作 command = msgRecvQueue.front(); msgRecvQueue.pop(); return true; } } //其他操作代码 return false; }

    不管信息接收队列(msgRecvQueue)是不是空,都要加锁解锁,大大降低了效率------这个问题在设计模式中单例模式也有说明

    进一步优化:第二种写法(正常优化写法)

    Copy
    //拿取数据的函数 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; }

    再进一步优化写法:第三种写法

    Copy
    //拿取数据的函数 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; }
  • 相关阅读:
    Windows 环境下的 Socket 编程 3 - 基于 TCP 的服务器/客户端
    FL Studio21.2破解版更新下载
    windows 离线安装 vue 环境
    计算机视觉与深度学习-循环神经网络与注意力机制-Attention(注意力机制)-【北邮鲁鹏】
    CTFHUB前置知识——http协议
    Acw_367
    如何使用vs code调试python程序
    java基于Spring boot+vue的线上教学考试平台 elementui
    (新)Spring Security如何实现跨域
    NLP 类问题建模方案探索实践
  • 原文地址:https://www.cnblogs.com/D-booker/p/16268382.html