为什么 conditional_variable::notify_all 可能不会唤醒任何线程?

问题描述

我使用 conditional_variable::notify_all() 来唤醒一个等待的线程(只有一个线程在等待 unique_lock 确实)。

代码段在大多数情况下运行良好,但日志文件(详细信息见下文)表明在新创建的线程已全部返回后,父线程无法获取 unique_lock

如果能在这个问题上得到一些帮助,我将不胜感激。

这是相关的代码片段:

void MainWindow::deployAction(void)
{
    std::condition_variable cvRunOver;
    std::mutex mtxRunOver;
    std::unique_lock <std::mutex> ulkRunOver(mtxRunOver);
    QString workerThreadRes;
    std::thread workThread([&]()
    {
        workThread.detach();

        do_some_process_for_seconds();
        
        cvRunOver.notify_all();
        LOG(INFO)<<"to leave the subthread";
        google::FlushLogFiles(google::GLOG_INFO);
        return;
    });

    while (cvRunOver.wait_for(ulkRunOver,std::chrono::milliseconds(100)) == std::cv_status::timeout)
    {
        qApp->processEvents();
        auto curTim = std::chrono::steady_clock::Now();
        std::chrono::duration<float> escapedTim= curTim-lastTim;
        if(std::chrono::duration_cast<std::chrono::seconds>(escapedTim).count()>=5)
        {
            LOG(INFO) << "processEvents()";
            google::FlushLogFiles(google::GLOG_INFO);
            lastTim = curTim;
        }
    }
    
    LOG(INFO) << "get lock and continue to run";
    google::FlushLogFiles(google::GLOG_INFO);
}

当程序无法正常工作时,相关日志如下:

Log line format: [IWEF]hh:mm:ss.uuuuuu threadid file:line] msg
20:19:14.638686 272568 mainwindow.cpp:208] to leave the subthread
20:19:17.669246 10256 mainwindow.cpp:221] processEvents()
20:19:22.678846 10256 mainwindow.cpp:221] processEvents()
20:19:17.669246 10256 mainwindow.cpp:221] processEvents()
20:19:22.678846 10256 mainwindow.cpp:221] processEvents()
20:19:17.669246 10256 mainwindow.cpp:221] processEvents()
20:19:22.678846 10256 mainwindow.cpp:221] processEvents()
20:19:17.669246 10256 mainwindow.cpp:221] processEvents()
...

解决方法

您滥用了条件变量。使用条件变量:

  1. 一个线程必须通知另一个线程共享状态的一些变化。

  2. 实际上肯定有一些共享状态发生了变化。

  3. 共享状态必须由与条件变量关联的互斥锁保护。

  4. 在决定等待之前必须测试共享状态。

  5. 执行信号或广播的线程在发出信号或广播之前必须在互斥锁的保护下改变了共享状态。

如果您不遵循这四个规则,您的代码将总是失败。您似乎没有任何受互斥锁保护的共享状态,您正在使用条件变量通知另一个线程其更改。没有这个,你就无法做出是否等待的正确决定,你最终会等待已经发生的事情。

有关详细信息,请参阅 this answer

想象一下,如果你和你妹妹共用一辆车。你让你姐姐在她把车开回来时按铃,这样你就可以不用等车了。现在想象一下你想使用汽车,所以你等待铃响。当你决定等待时,如果你姐姐没有用车,你会等待很长时间!

你的代码有这个缺陷,因为你的代码决定等待而不先检查它正在等待的事情是否已经发生,这违反了规则 4。你似乎也违反了规则 3,因为我没有看到任何受保护的共享状态互斥锁。您可能违反了规则 5,因为我没有看到您的 workThread 在调用通知函数之前更改任何共享状态。

我在 here 的示例代码中添加了一些注释,以展示所有规则的工作原理:

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

    std::mutex mtx;
    std::condition_variable cv;
    bool ready = false;

    void print_id (int id) {
      std::unique_lock<std::mutex> lck(mtx);
      while (!ready) cv.wait(lck); // rules 3 and 4 ("ready" is the shared state)
      // ...
      std::cout << "thread " << id << '\n';
    }

    void go() {
      std::unique_lock<std::mutex> lck(mtx); // rule 3
      ready = true; // rules 1 and 2
      cv.notify_all(); // rule 5
    }

    int main ()
    {
      std::thread threads[10];
      // spawn 10 threads:
      for (int i=0; i<10; ++i)
        threads[i] = std::thread(print_id,i);

      std::cout << "10 threads ready to race...\n";
      go();                       // go!

      for (auto& th : threads) th.join();

      return 0;
    }
,

大卫的回答非常好。我只想澄清几点。看这张图:

Consumer-Producer

一个线程为粉色,另一个为蓝色,同步机制为绿色。

条件变量的主要思想是启用被动同步。被动我的意思是不会在绝望的 while 循环 (while (!producer.has.data()) continue;) 中耗尽 CPU。因此,您需要一些共享数据,这些数据将随着程序的发展而改变。您需要一个互斥锁来保护数据再次竞争条件。那么条件变量是安眠药和闹钟的组合。

请注意,共享数据只能在锁定的互斥锁下才能访问。

请记住,醒来就像按铃一次。如果你想唤醒的线程没有睡着,它就会错过闹钟。通常这就是你想要的:如果消费者没有睡着,它不需要你的数据(还)。如果它需要数据,它会使用它而不会睡着。所以你可以把制作人想象成查尔斯卓别林,站在传送带旁,每次他“制作”一些东西时,他都会按铃。但他不知道,也不关心,是否有人能听到。也许这就是为什么该函数被称为“通知”而不是信号的原因,因为通常必须接收信号。通知没有。

图中有一个神秘的“OS”(操作系统)元素。是的,由条件变量“控制”的线程可能会被操作系统直接唤醒。这就是一些操作系统的工作方式。也许他们想确保没有线程死掉。所以当你醒来时,你必须,必须,必须检查你是被生产者唤醒,还是被操作系统唤醒。为此,您需要检查与共享数据状态相关的 CONDITION。所以你需要获取锁(这是自动和原子地完成的,图中并不清楚)并读取一些共享数据。它可以只是一个普通的 bool shared_ready,表示“数据已准备好/未准备好”或数据的条件,例如“!shared_container.empty()”。

在图中,生产者在处于锁定状态时通知另一个线程。不需要这样,顺序可以颠倒(先解锁,再通知其他线程)。

如果你已经走到这一步,你就可以在 cppreference Condition Variable

请看一下那里的示例。如何使用 lambda 来检查条件。这是使用条件变量的首选方式:使用它,您不能忘记 CONDITION!