Condition variables are fundamental synchronization primitives in multithreaded programming that enable threads to efficiently wait for specific conditions to become true. Unlike busy waiting, condition variables provide a mechanism for threads to suspend execution until another thread signals that a particular condition has been met, making them essential for efficient thread coordination in operating systems.
Understanding Condition Variables
A condition variable is a synchronization object that allows threads to wait until a particular condition occurs. They work in conjunction with mutexes to provide a complete synchronization solution. The key advantage of condition variables is that they eliminate the need for polling or busy waiting, which wastes CPU cycles.
Core Components and Operations
Condition variables consist of three primary operations:
- Wait: Atomically releases the associated mutex and blocks the calling thread until signaled
- Signal: Wakes up one waiting thread
- Broadcast: Wakes up all waiting threads
POSIX Implementation Example
Here’s a practical implementation demonstrating condition variables in C using POSIX threads:
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
typedef struct {
pthread_mutex_t mutex;
pthread_cond_t condition;
int counter;
int threshold;
} shared_data_t;
shared_data_t shared = {
.mutex = PTHREAD_MUTEX_INITIALIZER,
.condition = PTHREAD_COND_INITIALIZER,
.counter = 0,
.threshold = 5
};
void* consumer_thread(void* arg) {
int thread_id = *(int*)arg;
pthread_mutex_lock(&shared.mutex);
while (shared.counter < shared.threshold) {
printf("Consumer %d: Waiting for counter to reach %d (current: %d)\n",
thread_id, shared.threshold, shared.counter);
// Wait releases mutex and blocks until signaled
pthread_cond_wait(&shared.condition, &shared.mutex);
}
printf("Consumer %d: Condition met! Counter = %d\n", thread_id, shared.counter);
pthread_mutex_unlock(&shared.mutex);
return NULL;
}
void* producer_thread(void* arg) {
int thread_id = *(int*)arg;
for (int i = 0; i < 3; i++) {
sleep(1); // Simulate work
pthread_mutex_lock(&shared.mutex);
shared.counter++;
printf("Producer %d: Incremented counter to %d\n", thread_id, shared.counter);
if (shared.counter >= shared.threshold) {
// Signal all waiting consumers
pthread_cond_broadcast(&shared.condition);
printf("Producer %d: Broadcasting condition signal\n", thread_id);
}
pthread_mutex_unlock(&shared.mutex);
}
return NULL;
}
int main() {
pthread_t consumers[2], producers[2];
int consumer_ids[] = {1, 2};
int producer_ids[] = {1, 2};
// Create consumer threads
for (int i = 0; i < 2; i++) {
pthread_create(&consumers[i], NULL, consumer_thread, &consumer_ids[i]);
}
// Create producer threads
for (int i = 0; i < 2; i++) {
pthread_create(&producers[i], NULL, producer_thread, &producer_ids[i]);
}
// Wait for all threads to complete
for (int i = 0; i < 2; i++) {
pthread_join(consumers[i], NULL);
pthread_join(producers[i], NULL);
}
printf("Final counter value: %d\n", shared.counter);
return 0;
}
Expected Output
Consumer 1: Waiting for counter to reach 5 (current: 0)
Consumer 2: Waiting for counter to reach 5 (current: 0)
Producer 1: Incremented counter to 1
Producer 2: Incremented counter to 2
Producer 1: Incremented counter to 3
Producer 2: Incremented counter to 4
Producer 1: Incremented counter to 5
Producer 1: Broadcasting condition signal
Consumer 1: Condition met! Counter = 5
Consumer 2: Condition met! Counter = 5
Producer 2: Incremented counter to 6
Final counter value: 6
Producer-Consumer Pattern Implementation
The producer-consumer pattern is a classic use case for condition variables. Here’s a comprehensive implementation with a bounded buffer:
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#define BUFFER_SIZE 5
#define NUM_ITEMS 10
typedef struct {
int buffer[BUFFER_SIZE];
int count;
int in;
int out;
pthread_mutex_t mutex;
pthread_cond_t not_full;
pthread_cond_t not_empty;
} bounded_buffer_t;
bounded_buffer_t buffer = {
.count = 0,
.in = 0,
.out = 0,
.mutex = PTHREAD_MUTEX_INITIALIZER,
.not_full = PTHREAD_COND_INITIALIZER,
.not_empty = PTHREAD_COND_INITIALIZER
};
void put_item(int item) {
pthread_mutex_lock(&buffer.mutex);
// Wait while buffer is full
while (buffer.count == BUFFER_SIZE) {
printf("Buffer full, producer waiting...\n");
pthread_cond_wait(&buffer.not_full, &buffer.mutex);
}
// Add item to buffer
buffer.buffer[buffer.in] = item;
buffer.in = (buffer.in + 1) % BUFFER_SIZE;
buffer.count++;
printf("Produced item %d (buffer count: %d)\n", item, buffer.count);
// Signal that buffer is not empty
pthread_cond_signal(&buffer.not_empty);
pthread_mutex_unlock(&buffer.mutex);
}
int get_item() {
pthread_mutex_lock(&buffer.mutex);
// Wait while buffer is empty
while (buffer.count == 0) {
printf("Buffer empty, consumer waiting...\n");
pthread_cond_wait(&buffer.not_empty, &buffer.mutex);
}
// Remove item from buffer
int item = buffer.buffer[buffer.out];
buffer.out = (buffer.out + 1) % BUFFER_SIZE;
buffer.count--;
printf("Consumed item %d (buffer count: %d)\n", item, buffer.count);
// Signal that buffer is not full
pthread_cond_signal(&buffer.not_full);
pthread_mutex_unlock(&buffer.mutex);
return item;
}
void* producer(void* arg) {
for (int i = 1; i <= NUM_ITEMS; i++) {
put_item(i);
usleep(100000); // 100ms delay
}
return NULL;
}
void* consumer(void* arg) {
for (int i = 0; i < NUM_ITEMS; i++) {
get_item();
usleep(150000); // 150ms delay
}
return NULL;
}
Spurious Wakeups and Best Practices
One critical aspect of condition variables is handling spurious wakeups – situations where a thread wakes up from pthread_cond_wait() even though no thread called pthread_cond_signal() or pthread_cond_broadcast(). This is why condition checks should always be performed in loops:
Correct Pattern
// CORRECT: Always use while loops
pthread_mutex_lock(&mutex);
while (!condition_is_met) {
pthread_cond_wait(&condition, &mutex);
}
// Critical section
pthread_mutex_unlock(&mutex);
// INCORRECT: Don't use if statements
pthread_mutex_lock(&mutex);
if (!condition_is_met) { // This is wrong!
pthread_cond_wait(&condition, &mutex);
}
// Critical section may execute when condition is false
pthread_mutex_unlock(&mutex);
Advanced Use Cases
Read-Write Lock Implementation
Condition variables can be used to implement complex synchronization primitives like read-write locks:
typedef struct {
pthread_mutex_t mutex;
pthread_cond_t readers_proceed;
pthread_cond_t writer_proceed;
int active_readers;
int waiting_writers;
int active_writer;
} rwlock_t;
void reader_lock(rwlock_t* rw) {
pthread_mutex_lock(&rw->mutex);
while (rw->active_writer || rw->waiting_writers > 0) {
pthread_cond_wait(&rw->readers_proceed, &rw->mutex);
}
rw->active_readers++;
pthread_mutex_unlock(&rw->mutex);
}
void reader_unlock(rwlock_t* rw) {
pthread_mutex_lock(&rw->mutex);
rw->active_readers--;
if (rw->active_readers == 0 && rw->waiting_writers > 0) {
pthread_cond_signal(&rw->writer_proceed);
}
pthread_mutex_unlock(&rw->mutex);
}
void writer_lock(rwlock_t* rw) {
pthread_mutex_lock(&rw->mutex);
rw->waiting_writers++;
while (rw->active_readers > 0 || rw->active_writer) {
pthread_cond_wait(&rw->writer_proceed, &rw->mutex);
}
rw->waiting_writers--;
rw->active_writer = 1;
pthread_mutex_unlock(&rw->mutex);
}
Performance Considerations
Optimization Strategies
- Use signal() instead of broadcast() when possible: Only wake the minimum number of threads needed
- Minimize critical section size: Reduce the time mutex is held
- Consider predicate evaluation cost: Make condition checks efficient
- Avoid unnecessary wakeups: Only signal when the condition actually changes
Common Pitfalls and Debugging
Deadlock Prevention
// DEADLOCK SCENARIO: Wrong order of operations
void wrong_implementation() {
pthread_mutex_lock(&mutex1);
pthread_mutex_lock(&mutex2); // Different order in different threads
// ... work ...
pthread_mutex_unlock(&mutex2);
pthread_mutex_unlock(&mutex1);
}
// CORRECT: Consistent lock ordering
void correct_implementation() {
// Always acquire locks in the same order
if (&mutex1 < &mutex2) {
pthread_mutex_lock(&mutex1);
pthread_mutex_lock(&mutex2);
} else {
pthread_mutex_lock(&mutex2);
pthread_mutex_lock(&mutex1);
}
// ... work ...
pthread_mutex_unlock(&mutex2);
pthread_mutex_unlock(&mutex1);
}
Lost Wakeup Prevention
// WRONG: Signal before acquiring mutex
void signal_before_lock() {
condition_met = 1;
pthread_cond_signal(&condition); // Signal may be lost
pthread_mutex_lock(&mutex);
pthread_mutex_unlock(&mutex);
}
// CORRECT: Signal while holding mutex
void signal_with_lock() {
pthread_mutex_lock(&mutex);
condition_met = 1;
pthread_cond_signal(&condition); // Signal is guaranteed to be seen
pthread_mutex_unlock(&mutex);
}
Cross-Platform Considerations
Different operating systems provide various implementations of condition variables:
- POSIX (Linux, macOS, Unix): pthread_cond_t with POSIX semantics
- Windows: Condition Variables API (Windows Vista+) or manual implementation with events
- C++11 and later: std::condition_variable for portable code
C++11 Modern Alternative
#include <condition_variable>
#include <mutex>
#include <thread>
#include <iostream>
class ThreadSafeCounter {
private:
mutable std::mutex mutex_;
std::condition_variable condition_;
int count_ = 0;
int threshold_;
public:
ThreadSafeCounter(int threshold) : threshold_(threshold) {}
void wait_for_threshold() {
std::unique_lock<std::mutex> lock(mutex_);
condition_.wait(lock, [this] { return count_ >= threshold_; });
std::cout << "Threshold reached: " << count_ << std::endl;
}
void increment() {
std::lock_guard<std::mutex> lock(mutex_);
++count_;
std::cout << "Count: " << count_ << std::endl;
if (count_ >= threshold_) {
condition_.notify_all();
}
}
};
Conclusion
Condition variables are essential synchronization primitives that enable efficient thread coordination by eliminating busy waiting and providing a clean mechanism for threads to wait for specific conditions. When used correctly with mutexes, they form the foundation for building complex concurrent systems with minimal overhead.
Key takeaways for using condition variables effectively:
- Always use condition variables with associated mutexes
- Check conditions in while loops to handle spurious wakeups
- Signal or broadcast only when conditions actually change
- Minimize critical section duration to improve performance
- Follow consistent lock ordering to prevent deadlocks
Understanding and properly implementing condition variables is crucial for any systems programmer working with multithreaded applications, as they provide the building blocks for creating robust, efficient concurrent programs that can scale across multiple CPU cores while maintaining data consistency and avoiding race conditions.








