Skip to content

Wait and signal – std::condition_variable

Basic functionality – unsafe usageUsing condition variables – the safe wayTime limited waitingExample: Waiting for one of two conditionsExample: WaitableCondition – simplified synchronization

Motivation

Within multithreaded environments a system’s reaction to some internal or external trigger may consist of a series of small steps executed by multiple threads. The steps may be executed at least partly in parallel. Within such a scenario there is often the need that a thread has to wait until some preprocessing has reached some state before he can start or proceed with his own processing.

A simple approach for the waiting thread might be polling: he repeatedly checks some shared data until he recognizes that the data’s state allows to start his own processing. If the state is not yet reached he sleeps for a small time interval and then repeats the check.

The typical disadvantages of this approach are:

  • The repeated checks consume cpu load. Furthermore accessing shared data may require the need for repeatedly locking access to the data which may delay the work of other active threads which have to process the data.
  • The reaction time depends on the time interval used for sleeping. Assume that at some point in time the conditions to start a thread’s work are reached. But the thread may sleep for that time interval before he repeats the check and recognizes that he can start his part of processing.

To avoid these disadvantages you can use

  • std::condition_variable
    Principle: Thread A waits on a condition, i.e. the thread is switched to inactive mode. Thread B finishes his actions and signals that the condition has become true. As a consequence thread A is triggered and continues his execution.

Basic functionality – unsafe usage

Situation:

Some thread A has processed an incoming trigger and wants to inform a waiting thread B to continue with his part of the work.

Schematic solution (still incomplete)

There is a condition variable accessible from both threads (e.g. the variable may be a class attribute and the threads are working with the same class instance):

// Data definition shared by all threads

#include <condition_variable>
std::condition_variable myCondVar

Thread A finishes his part of the work and informs the waiting thread B to start his action:

// Thread A
... // do some work

// Trigger thread B to proceed
myCondVar.notify_one();

// If there are multiple waiting threads
// you can also inform all of them
myCondVar.notify_all();

Thread B waits until he receives the signal to continue with processing:

// Thread B
...

// Wait indefinitely until Thread A gives allowance to proceed
myCondVar.wait(..);

// now continue with processing

Attention: This is unsafe code!
The code snippets above will work in many cases and most of the time. But you may have sporadic erraneous behaviour because of the following reasons:

  • Thread B must be ready to be triggered
    Assume that thread B has not yet called “wait()” at the time when thread A calls “notify_one()”. When afterwards thread B calls wait() he will wait for ever. Calling notify_one() emits a signal to waiting threads only. Despite of the name “condition” the condition variable does not store the information that notify has already been called. A subsequent call of wait() will only return when after having entered the call of wait() a notify emitted by some other thread will arrive.
  • wait() may return without any trigger (“spurious wakeups”)
    Because of limitations in the threading library it may sometimes occur that wait() returns although Thread A has not yet called notify_one(). This means you cannot really be sure that you were waked up by thread A!

Because of these restrictions you cannot use the condition variable alone for proper synchronization. You always have to set and check your specific data when a decision is needed.

The condition variable is only a trigger to repeat your checks. The meaning of that trigger is: “Now it’s a good time to recheck your conditions. But be prepared that they are still not fulfilled”.

Keep in mind: Never use a condition variable alone!
A condition variable must always be used together with a mutex and some specific data protected by this mutex!

The next section will describe a safe way of synchronizing with the help of condition variables.

Using condition variables – the safe way

Situation:
Some thread A has processed an incoming trigger and wants to inform a waiting thread B to continue with his part of the work.

Schematic solution
Both threads have access to some specific shared data, a corresponding mutex and the condition variable (e.g. the variables may be class attributes and the threads are working with the same class instance):

// Data definitions shared by all threads
#include <mutex>
#include <condition_variable>

// to store status information
SomeSpecificDataStructure mySpecificData;

// to protect access to specific data
std::mutex myDataMutex;

// to trigger rechecking of conditions
std::condition_variable myCondVar;

Basics: with separate checking and waiting

Thread A finishes his part of the work and informs the waiting thread B to start his action:

// Thread A
{
    ... // do some work
    {
        std::lock_guard<std::mutex> guard(myDataMutex); //<##
        // access specific data and prepare all
        // for continuation by thread B
        mySpecificData.SomeField = ..
    } // release lock and mutex

    // Trigger thread B to recheck conditions
    myCondVar.notify_one();
    ... // continue with some other work
}

If data conditions are not yet fulfilled thread B waits until he receives the signal to recheck conditions:

// Helper function
bool DataAreReadyForProcessing ()
{
    // check mySpecificData
    // (assumes we are within lock)
    return true/false;
}
// Thread B
{
    ...
    //--- wait until data are prepared ---
    std::unique_lock<std::mutex> uLock(myDataMutex);
    while(!DataAreReadyForProcessing())
    {
        myCondVar.wait(uLock); // unlocks while waiting
        // locks again when returning
        // now recheck conditions 
    }
    //--- process data ---
    // here the mutex is still/again locked and you can access data
    mySpecificData.SomeField = .. 

Pay attention to the following points within the code snippet:

  • if the data conditions are already fulfilled then you may not call wait() on the condition variable (otherwise you may be blocked forever)
  • you have to lock the mutex and pass it (within uLock) to function wait()
  • wait() will automatically unlock the mutex when the thread is set to wait state (otherwise thread A would not be able to change your specific data)
  • when wait() returns the mutex is automatically relocked. You can directly recheck the conditions of your data and also change the data

For more info see condition_variable – complete reference at CppReference.com

Preferable: Embed checking into wait()

There is also a specialized wait function which allows you to specify the checking code as a predicate. Using the boolean function from above you could simplify the code. The explicitly programmed while loop will disappear (the loop is executed within wait()):

// Thread B
{
    ...
    std::unique_lock<std::mutex> uLock(myDataMutex);
    myCondVar.wait(uLock, DataAreReadyForProcessing);
    mySpecificData.SomeField = ..
}

As an alternative to a boolean function you could also pass a function object or a lambda expression:

// passing a lambda expression
myCondVar.wait(uLock, []{return mySpecificData.readyToProcess;});

Time limited waiting

To avoid indefinite blocking of a thread when a condition does not come true you could specify a maximum wait time. The wait will return either when the condition is fulfilled or when the timeout has elapsed. It is your task to analyze the reason why wait() has returned.

When using the wait() function without predicate you get a return value std::cv_status::timeout or std::cv_status::no_timeout:

// waiting for timeout after 5 seconds
std::chrono::seconds timeoutPeriod = 5;
auto timePoint = std::chrono::system_clock::now() + timeoutPeriod;

std::unique_lock<std::mutex> uLock(myDataMutex);
while(!DataAreReadyForProcessing())
{
    if (myCondVar.wait_until(uLock, timePoint)
        == std::cv_status::timeout)
    {
        // data conditions where not fulfilled within
        // the time period; e.g. do some error handling
        break;
    }
}

When using the wait() function with predicate you get a boolean return value (false means “timeout has elapsed”):

if (myCondVar.wait_for(uLock, timeoutPeriod, DataAreReadyForProcessing))
{
    // data conditions where fulfilled
    // regular processing
}
else // timeout occured, conditions are not fulfilled
{
    // e.g. do some error handling
}

Tip: Always prefer passing a predicate to the wait() functions. Then your code will stay more simple.

Examples

Waiting for one of two conditions

Situation
You are using two worker threads each doing a different piece of work. You want to react as soon as one of the threads has done its part of the work.

Solution concept
Both worker threads are using the same condition variable but they set different boolean flags within your specific data. The predicate within the wait function then can check for these flags and apply arbitrary boolean expressions to them, e.g. flag1 || flag2.

Data definitions shared by all threads:

#include <mutex>
#include <condition_variable>

SomeSpecificDataStructure mySpecificData;
std::mutex myDataMutex;
std::condition_variable myCondVar

Thread A finishes his part of the work and informs the waiting thread C:

// Thread A
{
    ... // some processing
    {
        std::lock_guard<std::mutex> guard(myDataMutex);
        mySpecificData.threadAHasProcessedData = true; 
    }
    myCondVar.notify_one();
    ... // continue with some other work
}

In the same way thread B finishes his part of the work and informs the waiting thread C:

// Thread B
{
    ... // some processing
    {
        std::lock_guard<std::mutex> guard(myDataMutex);
        mySpecificData.threadBHasProcessedData = true; 
    }
    myCondVar.notify_one();
    ... // continue with some other work
}

Thread C reacts as soon as one of the threads A or B has done its work:

// Thread C
{
    ... 
    std::unique_lock<std::mutex> uLock(myDataMutex);
    myCondVar.wait(uLock, []{
        return mySpecificData.threadAHasProcessedData
            || mySpecificData.threadBHasProcessedData;);

    ... // continue
}

Waiting for both threads

By changing the boolean expression within the lambda expression you can easily change to waiting until both threads have done their work. Simply replace “||” with “&&”.

WaitableCondition – simplified synchronization

Situation:
You only want to send a trigger from thread A to thread B to start its processing. You don’t have any data structures which need to be processed concurrently by both threads.

Solution concept:
Use a simple boolean variable as start condition. Embed it together with the corresponding access mutex and the condition variable within a thin wrapper of type WaitableCondition:

// Within your specific namespace MySyncUtils
class WaitableCondition
{
public:
    // Constructor
    WaitableCondition (bool in_autoReset = true)
        :
        m_autoReset  (in_autoReset),
        m_condIsTrue (false)
    {}

    // Set and reset the condition
    void Set (bool in_state)
    { 
        {
            std::lock_guard<std::mutex> guard(m_mutex);
            m_condIsTrue = in_state;
        }
        if (in_state) m_conditionVar.notify_one();
    }

    // Check condition without waiting
    bool IsTrue (void)
    {
        std::lock_guard<std::mutex> guard(m_mutex);
        return m_condIsTrue;
    }

    // Wait until condition becomes true
    // If the condition has already been set the function
    // will immediately return.
    void WaitUntilTrue (void)
    {
        std::unique_lock<std::mutex> uLock(m_mutex);
        m_conditionVar.wait(uLock,[]{return m_condIsTrue});
        if (m_autoReset) m_condIsTrue = false;
    }

    // Wait until condition becomes true (return true)
    // or the given timeout has elapsed (return false)
    bool TimedWaitUntilTrue (long in_timeoutMs)
    {
        std::chrono::milliseconds timeoutPeriod (in_timeoutMs);
        std::unique_lock<std::mutex> uLock(m_mutex);
        if (m_conditionVar.wait_for(uLock,timeoutPeriod,
            []{return m_condIsTrue}))
        {
            if (m_autoReset) m_condIsTrue = false;
            return true;
        }
        else // timeout
        {
            return false;
        }
    }
private:
    // Boolean condition several threads are interested in
    bool m_condIsTrue;

    // Access mutex for reading/writing m_condIsTrue
    std::mutex m_mutex;

    // Condition variable for triggering a waiting thread
    std::condition_variable m_conditionVar;

    // optionally reset the condition each time WaitUntilTrue()
    // or TimedWaitUntilTrue() has returned
    bool m_autoReset;
}

Autoreset
By using the autoreset feature of class WaitableCondition you could trigger thread B several times without any code changes.

Using class WaitableCondition

// Data definitions shared by all threads
MySyncUtils::WaitableCondition myWaitableCondition;

Thread A triggers thread B

// Thread A
{
    // prepare something
    // trigger thread B to proceed
    myWaitableCondition.Set(true);

    // continue processing
}

Thread B waits to be triggered by thread A:

// Thread B
{
    ..
    // Wait for trigger before continuing
    myWaitableCondition.WaitUntilTrue();

    // start/continue processing
}

Because of the embedded logic within WaitableCondition it is not necessary that thread B has already entered waiting state before thread A signals a change of condition.