why do I need std::condition_variable?

45,805

Solution 1

You can code this either way:

  1. Using atomics and a polling loop.
  2. Using a condition_variable.

I've coded it both ways for you below. On my system I can monitor in real time how much cpu any given process is using.

First with the polling loop:

#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();
}

For me this takes 30 seconds to execute, and while executing the process takes about 99.6% of a cpu.

Alternatively with a 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();
}

This has the exact same behavior except that during the 30 second execution, the process is taking 0.0% cpu. If you're writing an app that might execute on a battery powered device, the latter is nearly infinitely easier on the battery.

Now admittedly, if you had a very poor implementation of std::condition_variable, it could have the same inefficiency as the polling loop. However in practice such a vendor ought to go out of business fairly quickly.

Update

For grins I augmented my condition_variable wait loop with a spurious wakeup detector. I ran it again, and it did not print out anything. Not one spurious wakeup. That is of course not guaranteed. But it does demonstrate what a quality implementation can achieve.

Solution 2

The purpose of std::condition_variable is to wait for some condition to become true. It is not designed to be just a receiver of a notify. You might use it, for example, when a consumer thread needs to wait for a queue to become non-empty.

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.
}

The consumer (get_from_queue) is not waiting for the condition variable, they are waiting for the condition the_queue.empty(). The condition variable gives you the way to put them to sleep while they are waiting, simultaneously releasing the mutex and doing so in a way that avoids race conditions where you miss wake ups.

The condition you are waiting on should be protected by a mutex (the one you release when you wait on the condition variable.) This means that the condition rarely (if ever) needs to be an atomic. You are always accessing it from within a mutex.

Share:
45,805
Admin
Author by

Admin

Updated on April 05, 2020

Comments

  • Admin
    Admin about 4 years

    I found that std::condition_variable is very difficult to use due to spurious wakeups. So sometimes I need to set a flags such as:

    atomic<bool> is_ready;
    

    I set is_ready to true before I call notify (notify_one() or notify_all()), and then I wait:

    some_condition_variable.wait(some_unique_lock, [&is_ready]{
        return bool(is_ready);
    });
    

    Is there any reason that I shouldn't just do this: (Edit: Ok, this is really a bad idea.)

    while(!is_ready) {
        this_thread::wait_for(some_duration); //Edit: changed from this_thread::yield();
    }
    

    And if condition_variable had chosen a waiting duration (I don't know whether this is true or not), I prefer choose it myself.