Condition Variables

  • Suppose we have two threads

    • "Writer" thread modifies a shared string
    • "Reader" thread uses the modified string
  • The two threads need to be coordinated

  • We must also avoid a data race

  • One solution is to introduce a condition variable

  • Scenario

    • Thread A tells the condition variable it is waiting
    • Thread B notifies the condition variable when it updates the string
    • The condition variable wakes thread A up
    • Thread A then uses the string

Condition Variable and Thread Safety

  • We use a mutex to protect critical sections
  • The condition variable also uses the same mutex
    • Thread coordination
    • No data race

std::condition_variable

  • Defined in <condition_variable>

  • wait()

    • Takes an argument of type std::unique_lock
    • It unlocks its argument and blocks the thread until a notification is received
  • wait_for() and wait_until()

    • Relock their argument if a notification is not received in time
  • notify_one()

    • Wake up one of the waiting threads
    • The scheduler decides which thread is woken up
  • notify_all()

    • Wake up all the waiting threads

Condition Variable Scenario

  • Thread A locks the mutex

    • It calls the condition_variable's wait() member function
    • The condition variable unlocks the mutex
    • The condition variable blocks this thread
  • Thread B locks the mutex

    • It modifies the string and unlocks the mutex
    • It calls notify_one()
  • The condition variable wakes thread A up

    • The wait() call returns with the mutex locked
    • Thread A resumes execution and uses the string

Reader thread

// Waiting thread
void reader() {
  // Lock the mutex
  std::unique_lock<std::mutex> uniq_lck(mut);

  // Call wait() on the condition variable
  // Unlocks the mutex and makes this thread sleep
  cond_var.wait(uniq_lck);

  // The condition variable wakes this thread up and locks the mutex
  
  // Use the shared data
}

Writer thread

// Notifying thread
void writer() {
  {
  // Lock the mutex
  std::lock_guard<std::mutex> lck_guard(mut);

  // Modify the shared data
  sdata = "Populated";
  } // Release the lock
  
  // Notify the condition variable
  cond_var.notify_one();
}

Code Example


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

using namespace std::literals;

std::string sdata;

// To protect critical sections
std::mutex mut;

std::condition_variable cond_var;

void reader() {
  // Lock the mutex
  std::cout << "Reader thread locking mutex\n";
  std::unique_lock<std::mutex> uniq_lck(mut);
  std::cout << "Reader thread had locked the mutex\n";

  // Sleep until waken up by cond_var
  std::cout << "Reader thread sleeping...\n";
  cond_var.wait(uniq_lck);

  std::cout << "Reader thread wakes up\n";

  std::cout << "Data is \"" << sdata << "\"\n";
}

void writer() {
  {
    std::cout << "Writer thread locking mutex\n";

    // This will not be explicitly unlocked
    std::lock_guard<std::mutex> lck_guard(mut);
    std::cout << "Writer thread had locked the mutex\n";

    std::this_thread::sleep_for(2s);

    std::cout << "Writer thread modifying data...\n";
    sdata = "Populated";
  }

  // Notify the condition variable
  std::cout << "Writer thread sends notification\n";
  cond_var.notify_one();
}

int main() {
  sdata = "Empty";

  std::cout << "Data is \"" << sdata << "\"\n";

  std::thread read(reader);
  std::thread write(writer);

  write.join();
  read.join();
}

std::condition_variable_any

  • std::condition_variable only works with std::mutex

    • Does not work withstd::timed_mutex
  • There is also std::condition_variable_any

    • Works with any mutex-like object
    • Including our own types
    • May have more overhead than std::condition_variable