Modern Effective C++ - Using condition variables to communicate between threads

Usually the most popular choice when we need to communicate between threads is condition variable. Usually there is a sender who signals that the condition is true, and the receiver who waits till the sender's signal. The sender usually notifies the receiver(s) by using std::notify_one, or std::notify_all.

The code of the receiver code which has to wait till the sender's signal kind of "blocks" due cond_var.wait(mutex_var). But using condition variables is not a good idea in some cases, because of it's dependence on mutex. In some scenarios, the sender and receiver do not operate on shared data. Hence using mutex in order to signal an event is a definite overkill.


If you notice, the sole reason for the existence of mutex m1 is for the condition variable to use it. The flag is used to signal the runner threads to start running. But the threads (including main thread) do not operate on shared data.

There are two other problems in using condition variables. They are -

  • If the notify in sender is called before the wait in the receiver function, the receiver will miss the notification and it will be in the wait station forever. Below is a program demonstrating this problem


  • Spurious wakeups -Sometimes threads waiting on condition variables can wake up even without being notified. When this happens, the thread wakes up, checks that the condition of condition variable is still false and goes back to waiting state. This leads to wastage of CPU cycles.

In the next article we will see how we can eliminate the use of condition variables for such one shot communication between threads.


Modern Effective C++ - Using condition variables to communicate between threads Modern Effective C++ -  Using condition variables to communicate between threads Reviewed by zeroingTheDot on May 25, 2018 Rating: 5

2 comments:

  1. Do you mean std::future is more efficient than std::condition_variable ?
    According to boost::future (or boost::promise), it is based on boost::condition_variable, boost::mutex,
    boost::shared_ptr and many other logic, here's the data structure:

    struct shared_state_base : enable_shared_from_this
    {
    typedef std::list waiter_list;
    typedef waiter_list::iterator notify_when_ready_handle;
    // This type should be only included conditionally if interruptions are allowed, but is included to maintain the same layout.
    typedef shared_ptr continuation_ptr_type;
    typedef std::vector continuations_type;

    boost::exception_ptr exception;
    bool done;
    bool is_valid_;
    bool is_deferred_;
    bool is_constructed;
    launch policy_;
    mutable boost::mutex mutex;
    boost::condition_variable waiters;
    waiter_list external_waiters;
    boost::function callback;
    // This declaration should be only included conditionally, but is included to maintain the same layout.
    continuations_type continuations;
    executor_ptr_type ex_;

    And for spurious wake up, it seems has been resolved in future (by is_done()), but
    1. this mechanism is provided by condition_variable, so obviously, condition_variable is able to resolve spurious
    wake up (if you think future really resolved it), just call the wait/wait_for with a predicate (as future does)
    2. actually, spurious wake up is still happen (in wait_for in future or condition_variable with a predicate).

    So, I feel future is more heavy and less efficient than condition_variable, but future provides more functions.

    ReplyDelete
  2. And I found if the Futex is available, std::future is more efficient than std::condition_variable, so I recommend std::future too.
    But boost::future still have not introduced Futex or even atomic_future, it's just a wrapper of boost::condition_variable and boost::mutex.

    ReplyDelete

Powered by Blogger.