面向的问题
当一个线程等待另一个线程完成任务时,它会有很多选择。
- 第一,它可以持续的检查共享数据标志(用于做保护工作的互斥量),直到另一线程完成工作时对这个标志进行重设。不过,就是一种浪费:线程消耗宝贵的执行时间持续的检查对应标志,并且当互斥量被等待线程上锁后,其他线程就没有办法获取锁,这样线程就会持续等待。
- 第二个选择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。