为什么conditional_variable::notify_all可能没有唤醒任何线程?

Why conditional_variable::notify_all may not wake up any thread?

我用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. 执行信号或广播的线程必须在信号或广播之前在互斥锁的保护下更改共享状态。

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

有关详细信息,请参阅

想象一下,如果你和你姐姐共用一辆车。你让你姐姐在她把车开回来的时候按铃,这样你就可以不用等了。现在假设您想使用汽车,所以您等待铃声响起。如果你决定等的时候你姐姐没用车,你会等很久!

你的代码有这个缺陷,因为你的代码决定等待而不先检查它正在等待的事情是否已经发生,违反了规则 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;
    }

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

一个线程是粉红色的,另一个是蓝色的,同步机制是绿色的。

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

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

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

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

在图中,生产者在锁定时通知另一个线程。不需要这样,顺序可以反过来(先解锁,再通知其他线程)。

如果你已经走到这一步,你已经准备好接受 cppreference 中的专业描述 Condition Variable

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