【C++多线程】条件变量condition_variable

面向的问题

  当一个线程等待另一个线程完成任务时,它会有很多选择。

  • 第一,它可以持续的检查共享数据标志(用于做保护工作的互斥量),直到另一线程完成工作时对这个标志进行重设。不过,就是一种浪费:线程消耗宝贵的执行时间持续的检查对应标志,并且当互斥量被等待线程上锁后,其他线程就没有办法获取锁,这样线程就会持续等待。
  • 第二个选择l是周期轮询,在等待线程在检查间隙,使用 std::this_thread::sleep_for() 进行周期性的间歇在这个循环中,在休眠前,函数对互斥量进行解锁,并且在休眠结束后再对互斥量进行上锁,所以另外的线程就有机会获取锁并设置标识。
  • 第三个选择(也是优先的选择)是,使用C++标准库提供的条件变量condition_variable去等待事件的发生。通过另一线程触发等待事件的机制是最基本的唤醒方式。

condition_variable

  std::condition_variable 和 std::condition_variable_any 。这两个实现都包含在 <mutex> 或者<condition_variable>头文件的声明中。两者都需要与一个互斥量一起才能工作(互斥量是为了同步);std::condition_variable仅限于与 std::mutex 一起工作,而后者可以和任何满足最低标准的互斥量一起工作,从而加上了_any的后缀。因为 std::condition_variable_any 更加通用,这就可能从体积、性能,以及系统资源的使用方面产生额外的开销。

 wait()和notify_one()

1 std::mutex mymutex1;
2 std::unique_lock<std::mutex> sbguard1(mymutex1);
3 std::condition_variable condition;
4 condition.wait(sbguard1, [this] {if (!msgRecvQueue.empty())
5                                     return true;
6                                 return false;
7                                 });
8//没有第二个参数时
9 condition.wait(sbguard1);

   wait()用来等一个事件或者条件满足,如果第二个参数(可调对象)的lambda表达式返回值是false,即条件不满足,那么wait()将解锁互斥量,并阻塞到本行,如果第二个参数的lambda表达式返回值是true,那么wait()直接返回并继续执行。

  阻塞到什么时候为止呢?阻塞到其他某个线程调用notify_one()成员函数唤醒为止

  如果没有第二个参数,那么效果跟第二个参数lambda表达式返回false效果一样

  wait()将解锁互斥量,并阻塞到本行,直到到其他某个线程调用notify_one()成员函数为止。

  当其他线程用notify_one()将本线程wait()唤醒后,这个wait被唤醒后

  1、wait()不断尝试获取互斥量锁,如果获取不到那么流程就卡在wait()这里等待获取,如果获取到了,那么wait()就继续执行,获取到了锁

  2.1、如果wait有第二个参数就判断这个lambda表达式。

    a)如果表达式为false,那wait又对互斥量解锁,然后又休眠,等待再次被notify_one()唤醒
    b)如果lambda表达式为true,则wait返回,流程可以继续执行(此时互斥量已被锁住)。
  2.2、如果wait没有第二个参数,则wait返回,流程走下去。

  流程只要走到了wait()下面则互斥量一定被锁住了。

 1 #include <thread>
 2 #include <iostream>
 3 #include <list>
 4 #include <mutex>
 5 using namespace std;
 6  
 7 class A {
 8 public:
 9     void inMsgRecvQueue() {
10         for (int i = 0; i < 100000; ++i) 
11         {
12             cout << "inMsgRecvQueue插入一个元素" << i << endl;
13 
14             std::unique_lock<std::mutex> sbguard1(mymutex1);
15             msgRecvQueue.push_back(i); 
16             //尝试把wait()线程唤醒,执行完这行,
17             //那么outMsgRecvQueue()里的wait就会被唤醒
18             //只有当另外一个线程正在执行wait()时notify_one()才会起效,否则没有作用
19             condition.notify_one();
20         }
21     }
22  
23     void outMsgRecvQueue() {
24         int command = 0;
25         while (true) {
26             std::unique_lock<std::mutex> sbguard2(mymutex1);
27             // wait()用来等一个东西
28             // 如果第二个参数的lambda表达式返回值是false,那么wait()将解锁互斥量,并阻塞到本行
29             // 阻塞到什么时候为止呢?阻塞到其他某个线程调用notify_one()成员函数为止;
30             //当 wait() 被 notify_one() 激活时,会先执行它的 条件判断表达式 是否为 true,
31             //如果为true才会继续往下执行
32             condition.wait(sbguard2, [this] {
33                 if (!msgRecvQueue.empty())
34                     return true;
35                 return false;});
36             command = msgRecvQueue.front();
37             msgRecvQueue.pop_front();
38             //因为unique_lock的灵活性,我们可以随时unlock,以免锁住太长时间
39             sbguard2.unlock(); 
40             cout << "outMsgRecvQueue()执行,取出第一个元素" << endl;
41         }
42     }
43  
44 private:
45     std::list<int> msgRecvQueue;
46     std::mutex mymutex1;
47     std::condition_variable condition;
48 };
49  
50 int main() {
51     A myobja;
52     std::thread myoutobj(&A::outMsgRecvQueue, &myobja);
53     std::thread myinobj(&A::inMsgRecvQueue, &myobja);
54     myinobj.join();
55     myoutobj.join();
56 }

  上面的代码可能导致出现一种情况:因为outMsgRecvQueue()与inMsgRecvQueue()并不是一对一执行的,所以当程序循环执行很多次以后,可能在msgRecvQueue 中已经有了很多消息,但是,outMsgRecvQueue还是被唤醒一次只处理一条数据。这时可以考虑把outMsgRecvQueue多执行几次,或者对inMsgRecvQueue进行限流。

notify_all()

  同时通知所有等待线程,但是需要注意的是,如果所有线程只有一个线程可以拿到互斥量,那么也只有一个线程可以继续执行。

  对使用读写锁的多个读线程,可以同时被唤醒并同时继续工作。

拓展

  当等待一个一次性事件时,condition_variable显然不是最好的选择,这时需要的是future。

原文地址:https://www.cnblogs.com/chen-cs/p/13236638.html