【问题标题】:Why conditional_variable::notify_all may not wake up any thread?为什么 conditional_variable::notify_all 可能不会唤醒任何线程?
【发布时间】:2021-06-10 01:57:03
【问题描述】:

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

这段代码sn-p大部分时候运行良好,但是日志文件(详见下文)表明,在新创建的线程全部返回后,父线程无法获取unique_lock

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

这里是相关代码sn-p:

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()
...

【问题讨论】:

  • notify_all 未排队。如果它在目标线程唤醒并运行时执行(这里:在 while 循环内),它将丢失。使用带有谓词的wait_for 作为第三个参数来检测主线程实际上已发出“唤醒”信号并避免虚假唤醒。我被教导永远不要单独使用条件变量,而是使用带有一些信息的共享变量(例如,“正常”布尔值),例如“数据已准备好,您现在可以处理它”。
  • @zkoza 感谢您的澄清。能否请您给我一个简单的代码 sn-p 以使其更清晰?
  • 在整段代码中workThread 是否持有ulkRunover?这意味着在工作时要持有锁,这是没有意义的。 workThread 在哪里修改共享状态?实际上,等待的共享状态在哪里?
  • @David Schwartz >“在整个代码段中,workThread 是否持有 ulkRunover”?我想是这样。你看到Lamda函数的捕获子句是&amp;。所以workThread会在线程开始运行时获取这个锁。

标签: c++ qt c++11 condition-variable unique-lock


【解决方案1】:

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

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

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

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

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

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

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

更多信息请参见this answer

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

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

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

    // 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;
    }

【讨论】:

  • 感谢您的详细解释。我会仔细研究它。
【解决方案2】:

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

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

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

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

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

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

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

如果您已经走到了这一步,那么您已经准备好在 cppreference Condition Variable 中进行专业描述了

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-12-04
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多