为什么我需要std :: condition_variable?

52 c++ concurrency c++11

我发现std::condition_variable由于虚假的唤醒很难使用.所以有时我需要设置一个标志,如:

atomic<bool> is_ready;
Run Code Online (Sandbox Code Playgroud)

我在调用notify(或)之前设置is_ready为,然后我等待:truenotify_one()notify_all()

some_condition_variable.wait(some_unique_lock, [&is_ready]{
    return bool(is_ready);
});
Run Code Online (Sandbox Code Playgroud)

有什么理由我不应该这样做:(编辑:好的,这真是一个坏主意.)

while(!is_ready) {
    this_thread::wait_for(some_duration); //Edit: changed from this_thread::yield();
}
Run Code Online (Sandbox Code Playgroud)

如果condition_variable选择了等待时间(我不知道这是否属实),我宁愿自己选择.

How*_*ant 86

你可以用以下两种方式编码:

  1. 使用原子和轮询循环.
  2. 用一个condition_variable.

我在下面为你编写了两种方式.在我的系统上,我可以实时监控任何给定进程使用的CPU数量.

首先是轮询循环:

#include <atomic>
#include <chrono>
#include <iostream>
#include <thread>

std::atomic<bool> is_ready(false);

void
test()
{
    std::this_thread::sleep_for(std::chrono::seconds(30));
    is_ready.store(true);
}

int
main()
{
    std::thread t(test);
    while (!is_ready.load())
        std::this_thread::yield();
    t.join();
}
Run Code Online (Sandbox Code Playgroud)

对我来说,执行需要30秒,而执行该过程需要大约99.6%的CPU.

另外还有一个condition_variable:

#include <chrono>
#include <condition_variable>
#include <iostream>
#include <mutex>
#include <thread>

bool is_ready(false);
std::mutex m;
std::condition_variable cv;

void
test()
{
    std::this_thread::sleep_for(std::chrono::seconds(30));
    std::unique_lock<std::mutex> lk(m);
    is_ready = true;
    cv.notify_one();
}

int
main()
{
    std::thread t(test);
    std::unique_lock<std::mutex> lk(m);
    while (!is_ready)
    {
        cv.wait(lk);
        if (!is_ready)
            std::cout << "Spurious wake up!\n";
    }
    t.join();
}
Run Code Online (Sandbox Code Playgroud)

这具有完全相同的行为,除了在30秒执行期间,该过程采用0.0%cpu.如果您正在编写可能在电池供电的设备上执行的应用程序,则后者在电池上几乎无限容易.

现在std::condition_variable可以肯定的是,如果你的执行非常糟糕,那么它可能与轮询循环具有相同的低效率.但实际上,这样的供应商应该很快就会破产.

更新

对于grins我用一个虚假的唤醒检测器扩充了我的condition_variable等待循环.我再次跑了,它没有打印出任何东西.没有一个虚假的唤醒.这当然不能保证.但它确实证明了质量实施可以实现的目标.

  • @Mike:但是现在你的消费者意识到is_ready现在是真的,你平均等待.5毫秒(大约100万个机器周期!).使用condition_variable,你不会烧掉任何cpu,并且可能会在几千个机器周期内被唤醒并检查状态. (10认同)
  • @bRadGibson:是的.第二个例子显然具有低能耗的好处,为了达到这个目的,你需要`mutex`和`condition_variable`.因为无论如何都需要它们,可以在`mutex`下操纵`is_ready`.当这样做有明显的好处时,人们应该只拿出一把叫做"原子"的尖锐而危险的刀.在进行代码审查时,当你看到`atomic`时,是时候喝一杯新鲜咖啡,真正专注于代码.花一个小时而不是5分钟.如果您不需要`atomic`,请不要让您的代码审阅者通过额外的努力. (4认同)
  • 我已经测试了你的代码,你的说法是正确的.但我认为这是因为你的杯子碰巧闲置了.我用`this_thread :: sleep_for(chrono :: milliseconds(1))`替换了`this_thread :: yield()`,并且进程也占用了0.0%的cpu. (3认同)
  • 这个答案是尝试使用`std :: condition_variable`的一个很好的起点.我很少有新概念的成长痛苦,但条件变量的概念是我长期以来想要的.我很高兴看到它可以直接通过`std`. (3认同)

Wan*_*gic 30

目的std::condition_variable是等待一些条件成为现实.它不是设计为通知的接收者.例如,当消费者线程需要等待队列变为非空时,您可以使用它.

T get_from_queue() {
   std::unique_lock l(the_mutex);
   while (the_queue.empty()) {
     the_condition_variable.wait(l);
   }
   // the above loop is _exactly_ equivalent to the_condition_variable.wait(l, [&the_queue](){ return !the_queue.empty(); }
   // now we have the mutex and the invariant (that the_queue be non-empty) is true
   T retval = the_queue.top();
   the_queue.pop();
   return retval;
}

put_in_queue(T& v) {
  std::unique_lock l(the_mutex);
  the_queue.push(v);
  the_condition_variable.notify_one();  // the queue is non-empty now, so wake up one of the blocked consumers (if there is one) so they can retest.
}
Run Code Online (Sandbox Code Playgroud)

consumer(get_from_queue)等待条件变量,它们正在等待条件the_queue.empty().条件变量为您提供了在等待时让它们进入睡眠状态的方法,同时释放互斥锁并以避免因错过唤醒的竞争条件的方式这样做.

您正在等待的条件应该受到互斥锁(在您等待条件变量时释放的互斥锁)的保护.这意味着该条件很少(如果有的话)需要是一个atomic.您始终从互斥锁中访问它.