Skip to main content

Overview

A Condition Variable is a synchronization primitive that allows threads to wait for a specific condition to become true. Unlike mutexes and semaphores that control access to resources, condition variables enable threads to signal each other when important events occur.

When to Use Condition Variables

Event Notification

Alerting threads when a condition changes (e.g., buffer not empty)

State Changes

Waiting for shared state to reach a desired value

Producer-Consumer

Coordinating between producers and consumers with condition checks

Monitor Patterns

Building higher-level synchronization abstractions like monitors

How It Works

Condition variables implement a wait-notify pattern:
  • wait(): Thread suspends execution until notified
  • signal(): Wakes up one waiting thread (if any)

Typical Usage Pattern

1

Check Condition

Thread checks if desired condition is true
2

Wait if False

If condition not met, call wait() to suspend execution
3

Modify State

Another thread changes the shared state
4

Signal Change

Modifying thread calls signal() to wake a waiter
5

Re-check Condition

Awakened thread re-checks condition before proceeding
Always used with mutex: Condition variables are almost always paired with a mutex to protect the condition being checked. The mutex is released during wait and re-acquired before returning.

Implementation

Here’s the complete condition variable implementation from the simulator:
source/js/core/conditionVariable.js
// Variable de condicion basica con cola FIFO.
export class ConditionVariable {
  constructor(name = "Condition") {
    // Nombre para debug y para mostrar en mensajes si hace falta.
    this.name = name;
    // Cola de hilos que hicieron wait y quedaron dormidos.
    this.queue = [];
  }

  // Agrega un hilo a la cola de espera de la condicion.
  wait(thread) {
    // Evito duplicados por seguridad si el hilo intenta esperar otra vez.
    if (!this.queue.includes(thread)) this.queue.push(thread);
  }

  // Despierta a un hilo (el mas antiguo) si existe.
  signal() {
    if (this.queue.length === 0) return null;
    return this.queue.shift();
  }
}

Wait Operation

The wait() method adds a thread to the waiting queue:
wait(thread) {
  // Prevent duplicates if thread tries to wait again
  if (!this.queue.includes(thread)) {
    this.queue.push(thread);
  }
}
If a thread is already waiting and attempts to wait again (e.g., due to re-execution in the simulator), we avoid adding it twice. This maintains queue integrity and prevents the thread from being signaled multiple times.
Key characteristics:
  • Non-blocking: The method itself doesn’t block the thread (the caller does)
  • FIFO ordering: Uses push() to add to the back of the queue
  • Simple state: Just tracks which threads are waiting

Signal Operation

The signal() method wakes up one waiting thread:
signal() {
  if (this.queue.length === 0) return null;
  return this.queue.shift();
}
Behavior:
  • Returns null if no threads are waiting (no-op)
  • Removes and returns the thread at the front of the queue
  • FIFO: The longest-waiting thread is awakened first
Signal vs Broadcast: This implementation provides signal() which wakes one thread. Some condition variable implementations also provide broadcast() which wakes all waiting threads. In the simulator, broadcast behavior is achieved by repeatedly calling signal().

Usage in Monitor Pattern

Condition variables are the key building block for monitors - high-level synchronization constructs. Here’s how the library monitor uses them:
class LibraryMonitor {
  constructor() {
    this.mutex = new Mutex();
    this.readerQueue = new ConditionVariable("readers");
    this.writerQueue = new ConditionVariable("writers");
    this.activeReaders = 0;
    this.writerActive = false;
  }
  
  enterRead(thread) {
    // Implicit mutex.acquire()
    
    while (this.writerActive || this.writerQueue.length > 0) {
      // Wait until safe for readers
      this.readerQueue.wait(thread);
    }
    
    this.activeReaders++;
    // Implicit mutex.release()
  }
  
  exitRead() {
    this.activeReaders--;
    
    if (this.activeReaders === 0) {
      // Wake up waiting writer
      const writer = this.writerQueue.signal();
      if (writer) writer.state = "ready";
    }
  }
}

Wait-Signal Protocol

The standard pattern for using condition variables:
// Waiting thread
mutex.acquire();
while (!condition) {
  condVar.wait(thread);
  // Note: In full implementation, wait() would:
  // 1. Release mutex
  // 2. Block thread
  // 3. Re-acquire mutex when signaled
}
// Condition is true, do work
mutex.release();

// Signaling thread
mutex.acquire();
// Modify shared state
condition = true;
const thread = condVar.signal();
if (thread) {
  thread.state = "ready";
}
mutex.release();
Always use while, not if: Check the condition in a loop, not just once. When a thread is awakened, the condition might have changed again due to other threads.
// WRONG
if (!condition) condVar.wait();

// CORRECT
while (!condition) condVar.wait();

FIFO Queue Management

The condition variable maintains FIFO ordering:
// Add to back
this.queue.push(thread);

// Remove from front
return this.queue.shift();
This ensures:
  • Fairness: Threads are awakened in the order they waited
  • No starvation: Every waiting thread will eventually be signaled
  • Predictability: Deterministic wake-up order

Lost Wake-up Problem

A classic concurrency bug occurs when a signal is sent before a thread waits:
// Thread A
// 1. Checks condition (false)
// 2. About to call wait()...

// Thread B (interrupts)
// 3. Sets condition true
// 4. Calls signal() - but nobody waiting yet!

// Thread A (resumes)
// 5. Calls wait() - blocks forever!
Solution: Always hold the mutex when checking conditions and calling wait/signal:
// Thread A
mutex.acquire();
while (!condition) {
  condVar.wait(thread); // Atomically releases mutex and waits
}
mutex.release();

// Thread B
mutex.acquire();
condition = true;
condVar.signal();
mutex.release();

Example: Readers-Writers with Condition Variables

Here’s how condition variables coordinate readers and writers in the library scenario:
// Reader wants to enter
enterRead(thread) {
  // Check if already granted
  if (this.readerGranted.has(thread)) {
    this.readerGranted.delete(thread);
    this.activeReaders += 1;
    return true;
  }

  // Can enter if no writer active/waiting
  const writerWaiting = this.writerQueue.length > 0;
  if (!this.writerActive && !writerWaiting) {
    this.activeReaders += 1;
    return true;
  }

  // Must wait - add to condition variable queue
  thread.state = "blocked";
  thread.blockedBy = this;
  if (!this.readerQueue.includes(thread)) {
    this.readerQueue.push(thread);
  }
  return false;
}

// Last reader exits - signal waiting writer
exitRead() {
  if (this.activeReaders > 0) this.activeReaders -= 1;
  return this.wakeUpThreads();
}

wakeUpThreads() {
  const awakened = [];
  
  // Priority: wake writer if possible
  if (!this.writerActive && this.activeReaders === 0 && 
      this.writerQueue.length > 0) {
    const nextWriter = this.writerQueue.shift();
    nextWriter.state = "ready";
    nextWriter.blockedBy = null;
    this.writerGranted = nextWriter;
    awakened.push(nextWriter);
    return awakened;
  }
  
  // Otherwise wake all readers
  if (!this.writerActive && this.writerQueue.length === 0 && 
      this.readerQueue.length > 0) {
    while (this.readerQueue.length > 0) {
      const reader = this.readerQueue.shift();
      reader.state = "ready";
      reader.blockedBy = null;
      this.readerGranted.add(reader);
      awakened.push(reader);
    }
  }
  
  return awakened;
}

Visualizing Condition Variables

In the simulator (library scenario), you can observe:
  • Reader queue: Students waiting to read
  • Writer queue: Librarian waiting to update catalog
  • Wake-up events: When threads are signaled and change to ready state
  • Priority policy: Writers get priority to prevent starvation

Common Pitfalls

Spurious Wake-ups: In real systems, threads can wake up without being signaled. Always check the condition in a while loop:
while (!condition) condVar.wait();
Forgotten Signal: If no thread signals after changing the condition, waiting threads block forever:
condition = true;
// Forgot to call condVar.signal()!
Wrong Condition: Each condition variable should guard a specific logical condition. Don’t reuse the same condVar for unrelated conditions.

Signal vs Broadcast

Some condition variable implementations provide both operations:
const thread = condVar.signal();
if (thread) thread.state = "ready";
// Only one thread wakes up
When to use each:
  • signal(): When only one thread can make progress (e.g., one consumer can take an item)
  • broadcast(): When multiple threads can make progress (e.g., all readers can enter)

Library (Readers-Writers)

Students reading and librarian updating catalog using condition variables in a monitor

Key Takeaways

1

Event Signaling

Condition variables enable threads to notify each other of state changes
2

Always with Mutex

Must be paired with a mutex to protect the condition being checked
3

FIFO Wake-up

Threads are awakened in the order they waited
4

No Lost Wake-ups

Hold mutex during condition check and wait/signal to prevent race conditions
5

While Loop Pattern

Always check condition in a loop to handle spurious wake-ups

See Also

  • Monitors - High-level abstraction combining mutex with condition variables
  • Semaphores - Alternative signaling mechanism
  • Mutex - Required companion for condition variables