C++并发编程之 std::condition_variable的虚假唤醒

1、虚假唤醒产生原因

首先,我们在创建一个生产者和消费者的模型,生产者生产数据存放在容器中,而消费者,从容器中拿到数据,并且每次释放第一个数据。具体代码如下:

/*************************************************************************
      > File Name: thread_spurious_wakeup.cpp
      > Author: 小和尚念经敲木鱼
      > Mail:  
      > Created Time: Sat 16 Oct 2021 06:25:49 PM CST
 ***********************************************************************/

#include <iostream>              // std::cout
#include <thread>                // std::thread
#include <mutex>                 // std::mutex, std::unique_lock
#include <condition_variable>    // std::condition_variable
#include <vector>

using namespace std;

/************************************************************************
* 文件说明
* 1.虚假唤醒笔记demo
* 2.后续补充知识点
************************************************************************/
std::mutex g_Mtx;
std::vector<int> g_Data;
std::condition_variable g_ConVar;
static int g_produce_count = 0;

void ConsumeData()
{
  while (1) 
  {
    {
      std::unique_lock<std::mutex> lck(g_Mtx);
      g_ConVar.wait(lck);//如果先执行的是ProductData,但是ConsumeData没有执行到wait则,会丢失nofify信号。则一直阻塞
      std::cout << "Data size = " << g_Data.size() << "\tData =" << *g_Data.begin()<< std::endl;
      g_Data.erase(g_Data.begin());
    }
    std::chrono::milliseconds sleep_time(1);
    std::this_thread::sleep_for(sleep_time);
  }
}

void ProduceData()
{
  while (1) 
  {
    {
      std::unique_lock<std::mutex> lck(g_Mtx);
      g_Data.push_back(g_produce_count);
      g_produce_count++;
      g_ConVar.notify_all();
    }
    std::chrono::milliseconds sleep_time(5);
    std::this_thread::sleep_for(sleep_time);
  }
}

int main(int agc,char * agv[])                                                    
{

  std::cout << "[" << __FILE__ << "]" << " thread note" << std::endl;
  std::thread thread1(ProduceData);
  std::thread thread2(ConsumeData);

  if (thread1.joinable())
    thread1.join();

  if (thread2.joinable())
    thread2.join();

  return 0;
}
/******************************end of file******************************/

好的,从上面可以看到普通的生产-消费者模型,我们已经都构建好了。接下来提出问题,如果CPU线程调度,先调度了生产者线程,但是消费线程还在创建中,并没有执行到wait阻塞这等待,那么我们程序是不是就丢失了一个通知信号?因为我在生产者和消费者都是用的while循环,所以可以说,可以等下次信号,但是我们是不是丢了第一次的通知信号?那么我们怎么规避丢失第一次信号这个问题呢?

/*************************************************************************
      > File Name: thread_spurious_wakeup.cpp
      > Author: 小和尚念经敲木鱼
      > Mail:  
      > Created Time: Sat 16 Oct 2021 06:25:49 PM CST
 ***********************************************************************/

#include <iostream>              // std::cout
#include <thread>                // std::thread
#include <mutex>                 // std::mutex, std::unique_lock
#include <condition_variable>    // std::condition_variable
#include <vector>

using namespace std;

/************************************************************************
* 文件说明
* 1.虚假唤醒笔记demo
* 2.后续补充知识点
************************************************************************/

std::mutex g_Mtx;
std::vector<int> g_Data;
std::condition_variable g_ConVar;
static int g_produce_count = 0;

void ConsumeData()
{
  while (1) 
  {
    {
      std::unique_lock<std::mutex> lck(g_Mtx);
      if (g_Data.empty()) { 
        g_ConVar.wait(lck);
      }
      std::cout << "Data size = " << g_Data.size() << "\tData =" << *g_Data.begin()<< std::endl;
      g_Data.erase(g_Data.begin());
    }
    std::chrono::milliseconds sleep_time(1);
    std::this_thread::sleep_for(sleep_time);
  }
}

void ProduceData()
{
  while (1) 
  {
    {
      std::unique_lock<std::mutex> lck(g_Mtx);
      g_Data.push_back(g_produce_count);
      g_produce_count++;
      g_ConVar.notify_all();
    }
    std::chrono::milliseconds sleep_time(5);
    std::this_thread::sleep_for(sleep_time);
  }
}

int main(int agc,char * agv[])                                                    
{

  std::cout << "[" << __FILE__ << "]" << " thread note" << std::endl;
  std::thread thread1(ProduceData);
  std::thread thread2(ConsumeData);

  if (thread1.joinable())
    thread1.join();

  if (thread2.joinable())
    thread2.join();

  return 0;
}
/******************************end of file******************************/

好的,从上面的代码中,我们加了如下一行的处理:

  if (g_Data.empty()) { 
        g_ConVar.wait(lck);
      }

这行处理是,只要线程执行,并获取到资源,则进行数据的判断,如果判断数据队列为空,则就等待,那么我们是不是可以理解为:通过判定消息队列是否有数据,再进行挂起线程,这样就可以规避掉丢失信号的这种情况了。但是这时候还有一个问题,按照CPU的尿性,他没事会调起你这个消费线程试试,看看你是不是还活着,那么如果这时候这些唤醒就是无效的,因为没有数据可以消费,就是玩~~~ 嘿~~~!!~
这种就是我们不需要看到的,因为没事我不想调用它。怎么解决呢?这就是虚假唤醒了。

2、规避虚假唤醒

前面我们介绍了虚假唤醒的由来,但是有问题就有解决的办法。我们可以在线程中由阻塞状态到唤醒后的状态增加附加条件,如果不满足条件则继续等待,如下所示:

/*************************************************************************
      > File Name: thread_spurious_wakeup.cpp
      > Author: 小和尚念经敲木鱼
      > Mail:  
      > Created Time: Sat 16 Oct 2021 06:25:49 PM CST
 ***********************************************************************/

#include <iostream>              // std::cout
#include <thread>                // std::thread
#include <mutex>                 // std::mutex, std::unique_lock
#include <condition_variable>    // std::condition_variable
#include <vector>

using namespace std;

/************************************************************************
* 文件说明
* 1.虚假唤醒笔记demo
* 2.后续补充知识点
************************************************************************/

std::mutex g_Mtx;
std::vector<int> g_Data;
std::condition_variable g_ConVar;
static int g_produce_count = 0;

void ConsumeData()
{
  while (1) 
  {
    {
      std::unique_lock<std::mutex> lck(g_Mtx);
      while (g_Data.empty()) { //规避虚假唤醒
        g_ConVar.wait(lck);
      }
      std::cout << "Data size = " << g_Data.size() << "\tData =" << *g_Data.begin()<< std::endl;
      g_Data.erase(g_Data.begin());
    }
    std::chrono::milliseconds sleep_time(1);
    std::this_thread::sleep_for(sleep_time);
  }
}

void ProduceData()
{
  while (1) 
  {
    {
      std::unique_lock<std::mutex> lck(g_Mtx);
      g_Data.push_back(g_produce_count);
      g_produce_count++;
      g_ConVar.notify_all();
    }
    std::chrono::milliseconds sleep_time(5);
    std::this_thread::sleep_for(sleep_time);
  }
}

int main(int agc,char * agv[])                                                    
{

  std::cout << "[" << __FILE__ << "]" << " thread note" << std::endl;
  std::thread thread1(ProduceData);
  std::thread thread2(ConsumeData);

  if (thread1.joinable())
    thread1.join();

  if (thread2.joinable())
    thread2.join();

  return 0;
}
/******************************end of file******************************/

3、总结

虚假唤醒是比较容易犯的问题,但是我们在写bug的时候多注意下,应该能规避这种坑的,还是非常的银杏。最后,有问题的话,望大佬斧正啦~~~

上一篇:Pytorch学习笔记之数据操作篇(以实践为主导)


下一篇:java多线程高并发学习从零开始——初识volatile关键字