zoukankan      html  css  js  c++  java
  • 条件变量中的伪唤醒和唤醒丢失问题

      C++11标准库的条件变量为我们实现多线程直接通信带来的变量,如果对其提供的函数使用不当会给程序带来隐藏的问题。比如:伪唤醒和唤醒丢失问题。

    一、什么是伪唤醒和唤醒丢失

      先看代码如何使用条件变量:

     1  std::condition_variable cv;
     2  std::mutex gMtx;
     3  
     4 void Sender()
     5 {
     6      std::cout << "Ready Send notification." << std::endl;
     7      cv.notify_one();   // 发送通知
     8  }
     9  
    10 void Receiver()
    11 {
    12      std::cout << "Wait for notification." << std::endl;
    13      std::unique_lock<std::mutex> lck(gMtx);
    14      cv.wait(lck);    // 等待通知并唤醒继续执行下面的指令
    15      std::cout << "Process." << std::endl;
    16 }
    17  
    18  int main() 
    19  {
    20      std::thread sender(Sender);
    21      std::thread receiver(Receiver);
    22      sender.join();
    23      receiver.join();     
    24      return 0;
    25 }

      我们在主线程中开启了两个线程,分别是:通知线程和接收线程。一般情况下,接收线程在调用条件变量的wait函数时解锁并让线程挂起,当通知线程调用条件变量的notify_once或notify_all函数时,等待线程会被唤醒并自动上锁,继续执行后面的指令。看似没有毛病的代码逻辑,却存在严重的隐患。其中一种是线程随机启动导致的唤醒丢失,即:通信线程先启动并调用通知函数,但是接收线程还没有开始执行等待函数,如果不再次调用函数通知,等待会一直持续下去。这个是最容易发现和验证的问题,上面的主线程中启动线程的顺序就会概率性出现唤醒丢失的问题。我们可以模拟丢失情况(只需要让接收线程阻塞下)验证如下:

          

      伪唤醒顾名思义就是:通知线程还没有调用通知函数前,接收线程就从等待中唤醒了,继续执行后面的指令,导致业务逻辑出现问题。由于这个伪唤醒并不是代码编写的逻辑导致,所以实际很难出现。我们可以使用条件变量提供的wait_for函数模拟:

     1 void Sender()
     2 {
     3     // 阻塞10秒,后才发通知
     4     std::this_thread::sleep_for(std::chrono::seconds(10));
     5     std::cout << "Ready Send notification." << std::endl;
     6     cv.notify_one();
     7 }
     8 
     9 void Receiver()
    10 {
    11     std::cout << "Wait for notification." << std::endl;
    12     std::unique_lock<std::mutex> lck(gMtx);
    13     cv.wait_for(lck, std::chrono::seconds(2));  // 模拟假唤醒
    14     std::cout << "Process." << std::endl;
    15 }

      验证效果如下:

           

    二、如何解决伪唤醒和唤醒丢失问题

      C++标准库的条件变量总共提供了三个等待唤醒函数:wait、wait_for和wait_until,都分别提供带判断式的重载函数。

      1.wait函数

      上面的代码已经验证wait的非判断式版本无法解决上面的问题,所以wait_for和wait_until的非判断式版本也无法正常解决问,仅仅不会让等待一直持续,但是这样会导致逻辑出现问题。

      wait的判断式可以完美解决上面的问题,为什么?

            要想弄清楚为什么,需要知道wait判断式处理逻辑是什么。这里不进行深入探讨,直接给出结论:

     调用wait判断式函数的时候,进行如下逻辑处理:
     1.如果判断式返回真,直接返回wait函数;否则挂起当前线程进入等待并解锁,等待其他通知线程通知
     2.接收到其他通知线程发送的通知,再次执行步骤1.

      验证:

      1.判断式为真,不需要通知线程,结束等待:

     1 void Sender()
     2 {
     3     std::this_thread::sleep_for(std::chrono::seconds(5));
     4     std::cout << "Ready Send notification." << std::endl;
     5     cv.notify_one();
     6 }
     7 
     8 void Receiver()
     9 {
    10     std::cout << "Wait for notification." << std::endl;
    11     std::unique_lock<std::mutex> lck(gMtx);
    12     cv.wait(lck, []() {return true; }); 
    13 
    14     std::cout << "Process." << std::endl;
    15 }

      

      2.接收线程等待过程中锁是解开的:

     1 void Sender()
     2 {
     3     std::this_thread::sleep_for(std::chrono::seconds(5));
     4     std::unique_lock<std::mutex> lck(gMtx);
     5     std::cout << "Ready Send notification." << std::endl;
     6     cv.notify_one();
     7 }
     8 
     9 void Receiver()
    10 {
    11     std::unique_lock<std::mutex> lck(gMtx);
    12     std::cout << "Wait for notification." << std::endl;
    13     cv.wait(lck, []() {return false; });  // 会一直阻塞下去
    14 
    15     std::cout << "Process." << std::endl;
    16 }

      

      3.接收到通知线程通知,但是判断式为假,继续阻塞:

     1 void Sender()
     2 {
     3     std::unique_lock<std::mutex> lck(gMtx);
     4     std::cout << "Ready Send notification." << std::endl;
     5     cv.notify_one();
     6 }
     7 
     8 void Receiver()
     9 {
    10     std::unique_lock<std::mutex> lck(gMtx);
    11     std::cout << "Wait for notification." << std::endl;
    12     cv.wait(lck, []() {return send; });   // send未设置为true,一直阻塞
    13 
    14     std::cout << "Process." << std::endl;
    15 }

      

      4.接收到通知线程通知,判断式为真,结束等待,加锁:

     1 void Sender()
     2 {
     3     std::unique_lock<std::mutex> lck(gMtx);
     4     std::cout << "Ready Send notification." << std::endl;
     5     send = true;
     6     cv.notify_one();
     7 }
     8 
     9 void Receiver()
    10 {
    11     std::unique_lock<std::mutex> lck(gMtx);
    12     std::cout << "Wait for notification." << std::endl;
    13     cv.wait(lck, []() {return send; });
    14     try{
    15         lck.lock();                      // 验证已经加锁了.
    16     }
    17     catch (const std::exception& e){
    18         std::cout << "locker is locked. e <" << e.what() << ">" << std::endl;
    19     }
    20     std::cout << "Process." << std::endl;
    21 }

      

       通过上面的验证,说明了wait带判断式函数的处理逻辑是正确的。

      解决伪唤醒:如果通知线程没有发生通知前,发生伪唤醒的时候,wait函数会再次检查判断式是否为真,如果为真,就认为通知线程发送了通知;否则继续等待通知;

      解决唤醒丢失:如果通知线程先发生了通知,接收线程后执行wait函数时,会检查判断式是否为真,如果为真,就认为通知线程发送了通知;否则继续等待通知;

      所以,判断式内部实现很重要且线程安全的。

      2.wait_for函数

      上面的对wait函数的介绍已经可以知道wait_for非判断式函数是不能解决上面的问题,下面是wait_for判断式函数解决上面的问题:

     1 void Sender()
     2 {
     3     std::unique_lock<std::mutex> lck(gMtx);
     4     std::cout << "Ready Send notification." << std::endl;
     5     send = true;
     6     cv.notify_one();
     7 }
     8 
     9 void Receiver()
    10 {
    11     std::unique_lock<std::mutex> lck(gMtx);
    12     std::cout << "Wait for notification." << std::endl;
    13     while (!cv.wait_for(lck, std::chrono::seconds(1), []() {return send; })) { // 这里设置超时等待时间,如果超时继续并且send=false,继续等待.
    14         std::cout << "wait timeout." << std::endl;
    15     }
    16     std::cout << "Process." << std::endl;
    17 }

      通过分析也可以解决上面的问题,但是相对于wait带判断式函数的处理方式,性能不够好,代码比较冗余。

      3.wait_unitl函数

      wait_unitl函数和wait_for函数类似,wait_until是等待时间点,wait_for等待时间段。所以wait_until的非判断式函数无法解决上面的问题,wait_until的判断式函数可以,就是循环判断超时的时间点一定要同步更新,类似wait_for函数的处理方式,但是实现逻辑更加复杂,性能更加不好。

    三、总结

      通过研究条件变量的伪唤醒和唤醒丢失问题的同时,也把条件变量相关的函数熟悉了一遍,尤其是对判断式wait的函数内部逻辑进行模拟验证,这会更加加深同学们对条件变量的正确使用和合理使用。

    参考Condition Variables - ModernesCpp.com

  • 相关阅读:
    S3C2440的LCD虚拟显示测试
    arm-linux-gcc编译器测试
    韦东山教程ARM的时钟设置出现的问题及其解决方法
    程序在nor flash中真的可以运行吗?
    存储器的速度
    程序测试的方法
    对编程的一些思考

    [算法题] 字节流解析
    [C/C++]函数指针和函数分发表
  • 原文地址:https://www.cnblogs.com/smartNeo/p/14967684.html
Copyright © 2011-2022 走看看