Loading content...
We have established that mutual exclusion ensures safety (no concurrent critical section access) and progress ensures liveness (the system moves forward). But consider this scenario:
Two processes, P₀ and P₁, contend for a critical section. The algorithm satisfies mutual exclusion and progress. P₀ enters, exits, and immediately re-enters. P₁ is waiting. P₀ exits, immediately re-enters again. P₁ continues waiting. This pattern repeats indefinitely.
Is this acceptable?
The system is making progress—P₀ is executing its critical section repeatedly. Mutual exclusion holds—only one process is ever inside. But P₁ is starving—it may wait forever while P₀ monopolizes the resource.
The third fundamental requirement, Bounded Waiting, addresses exactly this unfairness. It guarantees that no process can be bypassed indefinitely—that every waiting process will eventually get its turn.
This page provides a comprehensive treatment of bounded waiting: its formal definition, its importance, how to design solutions that satisfy it, and how to verify the property analytically.
By the end of this page, you will understand the formal definition of bounded waiting, distinguish it clearly from progress, identify starvation in synchronization algorithms, design fair synchronization solutions, and verify bounded waiting through analysis.
Bounded waiting (also called starvation-freedom or finite bypass) ensures that no process waits indefinitely while other processes repeatedly enter their critical sections.
Bounded Waiting Requirement:
There exists a bound (limit) on the number of times that other processes are allowed to enter their critical sections after a process has made a request to enter its critical section and before that request is granted.
Let's unpack this carefully:
"There exists a bound" — A specific, finite number B exists. This bound might depend on the number of processes n but must be finite.
"on the number of times that other processes are allowed to enter" — We count entries by OTHER processes, not iterations or time. Each time another process enters its critical section, that's one count.
"after a process has made a request" — The counting starts when a process signals intent to enter (reaches the entry section).
"before that request is granted" — The counting stops when the requesting process enters its critical section.
Let request_time[i] be the time when process i enters its entry section, and grant_time[i] be the time when it enters its critical section. Let entries(j, t₁, t₂) count how many times process j entered its critical section between times t₁ and t₂.
Bounded Waiting Property:
∃B. ∀i. ∀request. Σⱼ≠ᵢ entries(j, request_time[i], grant_time[i]) ≤ B
In words: There exists a bound B such that for every process i and every request by that process, the total number of entries by other processes between the request and grant is at most B.
Many fair algorithms achieve a bound of n - 1, where n is the number of processes. This means that after a process makes a request, each other process can enter its critical section at most once before the requesting process gets its turn.
This bound corresponds to a FIFO (First-In, First-Out) discipline: processes are served in the order they requested.
We allow OTHER processes to enter some finite number of times rather than requiring immediate entry. This is because: (1) already-running critical sections must complete, (2) processes already ahead in any queue should go first, and (3) requiring immediate entry could itself cause problems. The key is that the wait is BOUNDED, not minimal.
123456789101112131415161718192021222324
// Illustration: Bounded Waiting with Bound B = n-1 (for n processes) // Timeline showing process requests and critical section entries: Time: ─────────────────────────────────────────────────────────────► P₀: ────request───────────────────────────────────enter CS───── ↑ ↓ └───── waiting period ──────────────┘ P₁: ───────────enter CS──────────────────────────────────────── ↑ └─ counts toward P₀'s bound (1) P₂: ─────────────────────enter CS────────────────────────────── ↑ └─ counts toward P₀'s bound (2) // If P₁ and P₂ EACH enter only once before P₀, and there are 3 processes:// Bound = n - 1 = 2, and P₀ waited through 2 entries ✓ Bounded waiting satisfied // VIOLATION would be:// P₁ enters, P₂ enters, P₁ enters again, P₂ enters again... // while P₀ never gets in. That's unbounded!Starvation is the condition where bounded waiting fails—a process waits indefinitely while others repeatedly access the critical section. Unlike deadlock (no progress) or livelock (active but no progress), starvation involves the system making progress while leaving specific processes behind.
Starvation is particularly insidious because:
| System | Starved Entity | Cause | Consequence |
|---|---|---|---|
| Web server | Requests from slow clients | Fast clients monopolize connection pool | Slow clients timeout repeatedly |
| Database | Low-priority queries | Continuous stream of high-priority queries | Reports never complete |
| File system | Large file operations | Many small file operations preempt | Backup jobs never finish |
| Network switch | Low-QoS traffic | High-QoS traffic saturation | Best-effort packets dropped |
| Memory allocator | Large allocations | Frequent small allocations fragment memory | Out-of-memory for large requests despite free memory |
Starvation raises ethical questions about system design:
Fairness vs. Throughput: Fair algorithms may sacrifice overall throughput to ensure no process is left behind. Is this tradeoff acceptable?
Explicit vs. Implicit Priorities: If high-value customers get better service, is that acceptable starvation? Where is the line?
Long-term vs. Short-term: A process that waited long may deserve priority (aging), but that affects currently-running processes.
Operating systems and concurrent systems generally aim for bounded waiting as a minimum guarantee—even low-priority processes will eventually complete, even if they must wait longer.
Starvation often goes unnoticed until it becomes severe. A process waiting slightly longer than usual doesn't trigger alarms. By the time starvation is detected (timeouts, angry users, failed SLAs), it may have been happening for a long time. Design for bounded waiting from the start—it's much harder to retrofit.
Achieving bounded waiting requires deliberate design. Several techniques are commonly employed:
The most straightforward approach: maintain a queue of waiting processes. When the critical section becomes free, the process at the head of the queue enters.
12345678910111213141516171819202122232425262728293031323334353637383940414243
// FIFO Queue Lock (conceptual implementation)// Guarantees bounded waiting with bound B = n-1 typedef struct { Queue waiting_queue; // FIFO queue of waiting process IDs bool lock_held; int current_holder;} FIFOLock; void acquire(FIFOLock *lock, int my_id) { // Atomically add ourselves to queue bool was_empty = atomic_enqueue(&lock->waiting_queue, my_id); if (!was_empty || lock->lock_held) { // Wait until we're at the head and lock is free while (queue_head(&lock->waiting_queue) != my_id || lock->lock_held) { // Busy wait or block } } // We're at head and lock is free lock->lock_held = true; lock->current_holder = my_id;} void release(FIFOLock *lock, int my_id) { // Remove ourselves from queue head atomic_dequeue(&lock->waiting_queue); lock->lock_held = false; // Next process (if any) will notice and proceed} /* * BOUNDED WAITING GUARANTEE: * * When a process requests entry (enqueues): * - At most n-1 other processes can be ahead in the queue * - Each process ahead enters exactly once before leaving the queue * - Therefore, at most n-1 entries occur before the requesting * process reaches the head * - Bound B = n-1 ✓ */Processes take numbered tickets on arrival. The process with the lowest ticket number (among waiting processes) enters next.
12345678910111213141516171819202122232425262728293031323334353637383940
// Ticket Lock implementation// Guarantees strict FIFO ordering, bounded waiting B = n-1 typedef struct { volatile int next_ticket; // Next ticket to dispense volatile int now_serving; // Ticket currently being served} TicketLock; void acquire(TicketLock *lock) { // Atomically get our ticket number int my_ticket = atomic_fetch_add(&lock->next_ticket, 1); // Wait until our ticket is being served while (lock->now_serving != my_ticket) { // Spin (or yield) // Could add: pause instruction, exponential backoff } // It's our turn!} void release(TicketLock *lock) { // Serve the next ticket lock->now_serving++;} /* * HOW IT WORKS: * * 1. next_ticket starts at 0, now_serving starts at 0 * 2. First process gets ticket 0, immediately enters (0 == 0) * 3. Second process gets ticket 1, waits for now_serving == 1 * 4. When first releases, now_serving becomes 1, second enters * * BOUNDED WAITING: * - Tickets are strictly ordered * - No process can be bypassed (tickets always increase) * - Each process ahead takes exactly one turn * - Bound B = number of tickets taken before mine ≤ n-1 */Aging dynamically increases the priority of waiting processes over time. Eventually, even the lowest-priority process becomes the highest priority.
Processes are served in a fixed circular order. Process 0, then 1, then 2, ..., then n-1, then back to 0. If a process isn't waiting when its turn comes, skip to the next waiting process.
FIFO queuing and ticket locks provide strict fairness but may have higher overhead. Aging is more flexible but the bound depends on the aging rate. Round-robin is simple but may waste turns on non-waiting processes. Choose based on your system's fairness requirements and performance constraints.
Let's analyze bounded waiting in some classical synchronization algorithms.
Peterson's algorithm (for two processes) provides bounded waiting with bound B = 1:
turn variable ensures the waiting process gets priority after the other completesProof sketch: When P₀ sets flag[0] = true and turn = 1, if P₁ enters (because it already set flag[1] = true and turn = 0), P₁ will then set turn = 0 when it requests next. At that point, P₀'s waiting condition flag[1] && turn == 1 becomes false (since turn is now 0), so P₀ enters. Thus P₁ can bypass P₀ at most once.
The simple test-and-set spinlock does NOT provide bounded waiting:
12345678910111213141516171819202122232425262728293031
// Simple test-and-set: NO bounded waiting void acquire() { while (test_and_set(&lock)) { // Keep trying }} void release() { lock = false;} /* * WHY NO BOUNDED WAITING: * * Consider 3 processes: P₀, P₁, P₂ * * Initial: lock = false * 1. P₀ acquires lock * 2. P₁ and P₂ start spinning * 3. P₀ releases (lock = false) * 4. P₁ and P₂ both race for lock * 5. P₂ wins (happens to execute TAS first) * 6. P₂ executes, releases * 7. P₀ comes back wanting lock, races with P₁ * 8. P₀ wins (again, timing luck) * 9. This pattern can repeat indefinitely with P₁ never winning * * There is no mechanism to prevent a process from being * repeatedly bypassed by "luckier" processes. */The bakery algorithm provides bounded waiting for n processes with bound B = n-1:
Bound: Since tickets are strictly ordered and no process can get a lower ticket after you, at most n-1 processes (those who took tickets before you) can enter before you.
| Algorithm | Bounded Waiting? | Bound B | Notes |
|---|---|---|---|
| Simple TAS spinlock | ✗ No | Unbounded | No fairness mechanism |
| Peterson's (2 process) | ✓ Yes | 1 | Turn variable ensures fairness |
| Dekker's (2 process) | ✓ Yes | 1 | Similar to Peterson's |
| Lamport's Bakery (n process) | ✓ Yes | n-1 | Ticket-based ordering |
| Ticket Lock | ✓ Yes | n-1 | FIFO via atomic tickets |
| MCS Lock | ✓ Yes | n-1 | Queue-based with local spinning |
| CLH Lock | ✓ Yes | n-1 | Queue-based, space efficient |
Notice that algorithms providing bounded waiting require more sophisticated mechanisms than simple test-and-set: turn variables, ticket counters, or explicit queues. This is not coincidence—fairness requires ordering, and ordering requires state that tracks request sequence.
Proving bounded waiting requires showing that a finite bound exists on bypassing. Here's a systematic approach.
123456789101112131415161718192021222324252627282930313233343536373839404142
// Proving Bounded Waiting for Peterson's Algorithm (2 processes) // Algorithm (process i, j = 1-i):// Entry: flag[i] = true; turn = j; while (flag[j] && turn == j) {}// Exit: flag[i] = false // PROOF: // Claim: After Pᵢ enters the entry section, Pⱼ can enter // the critical section at most once before Pᵢ enters. // Proof:// Let time t₀ be when Pᵢ sets flag[i] = true.// // Case 1: flag[j] = false at some point after t₀// Pᵢ's while condition is false, so Pᵢ enters.// Pⱼ entered 0 times after t₀. Bound satisfied.//// Case 2: flag[j] = true continuously after t₀// This means Pⱼ is in its entry, CS, or exit section.// // Subcase 2a: Pⱼ is in its exit section// Pⱼ will set flag[j] = false, reducing to Case 1.// Pⱼ entered 0 or 1 times. Bound satisfied.// // Subcase 2b: Pⱼ is in CS// Pⱼ will exit, set flag[j] = false, reducing to Case 1.// Pⱼ entered 1 time. Bound satisfied.// // Subcase 2c: Pⱼ is in entry section// Pⱼ must have set turn = i (giving priority to Pᵢ).// If Pⱼ set turn = i BEFORE Pᵢ set turn = j:// turn = j (Pᵢ wrote last), Pᵢ passes while, enters.// Pⱼ entered 0 times. Bound satisfied.// If Pⱼ set turn = i AFTER Pᵢ set turn = j:// turn = i, Pⱼ passes while, enters first.// But when Pⱼ next requests, it sets turn = i again.// Now Pᵢ sees turn = i, passes while, enters.// Pⱼ entered 1 time. Bound satisfied.//// In all cases, Pⱼ enters at most once before Pᵢ.// Therefore, bounded waiting holds with B = 1. QEDTo prove bounded waiting does NOT hold, construct a specific execution trace showing unbounded bypass:
1234567891011121314151617181920212223242526272829303132
// Proving Simple TAS Lacks Bounded Waiting (by counterexample) // Algorithm:// Entry: while (test_and_set(&lock)) {}// Exit: lock = false // COUNTEREXAMPLE CONSTRUCTION: // Setup: 2 processes P₀ and P₁ // Trace:// 1. P₀ acquires lock (TAS returns false)// 2. P₁ enters busy loop, wanting lock// 3. P₀ completes CS, sets lock = false// 4. P₀ immediately wants lock again, executes TAS// 5. P₀ wins race (executes TAS before P₁'s next check)// 6. P₀ now in CS again; P₁ still waiting// 7. Repeat from step 3... // This trace can continue indefinitely with P₀ always winning.// P₁ is bypassed unboundedly. // WHY THIS IS POSSIBLE:// - No ordering mechanism exists// - Whichever process executes TAS first (when lock is false) wins// - On many CPUs, P₀ has cache-line advantage (recently accessed lock)// - P₀ can systematically win every race // CONCLUSION: No bound B exists. Bounded waiting is violated. // Note: This is a valid execution trace, not just theoretical.// It occurs frequently on real systems under high contention.To PROVE bounded waiting, show that every possible execution path leads to entry within the bound. To DISPROVE it, find just ONE execution path that bypasses infinitely. The asymmetry makes disproving easier—one counterexample suffices, while proving requires exhaustive analysis.
Bounded waiting interacts with other synchronization properties in important ways. Understanding these relationships guides design decisions.
12345678910111213141516171819202122232425
Requirement Hierarchy (Weakest to Strongest): 1. MUTUAL EXCLUSION (Safety) ├── Requirement: At most one process in CS at a time ├── Failure: Data corruption └── A solution MUST have this 2. PROGRESS (Basic Liveness) ├── Requirement: If CS empty and someone wants in, someone enters ├── Failure: System-wide deadlock ├── Implies: Mutual exclusion alone is not sufficient └── A solution MUST have this 3. BOUNDED WAITING (Fairness/Strong Liveness) ├── Requirement: Each waiting process enters within finite bypasses ├── Failure: Individual starvation ├── Implies: Progress alone is not sufficient └── A correct solution SHOULD have this Implication chain:Bounded Waiting → Progress → Mutual Exclusion (If bounded waiting holds, progress must hold because every process including SOME process enters. If no process ever enters, every process violates bounded waiting.)Enforcing bounded waiting often comes at a cost to throughput:
The importance of bounded waiting depends on contention level:
Low contention: Bounded waiting matters less. If contention is rare, unfair locks rarely starve anyone in practice.
High contention: Bounded waiting is critical. Under heavy load, unfair locks can cause severe starvation of some threads while others monopolize access.
Variable contention: The hardest case. Must design for worst-case (high contention) while not paying excessive overhead during normal operation (low contention).
Modern lock implementations often use adaptive strategies:
In practice, most systems prefer locks with bounded waiting, accepting some throughput cost for predictability. Starvation causes unpredictable latency spikes that are difficult to diagnose and may violate SLAs. The overhead of fair locks is usually acceptable, especially with modern implementations like MCS or CLH locks.
Modern operating systems and runtime systems implement bounded waiting through various mechanisms.
POSIX mutexes (pthread_mutex) typically provide bounded waiting through kernel-maintained wait queues:
1234567891011121314151617181920212223242526272829303132333435363738394041
// Simplified view of how OS mutexes provide bounded waiting struct kernel_mutex { atomic_t state; // 0 = free, 1 = held spinlock_t wait_lock; // Protects the wait queue struct list_head wait_queue; // FIFO queue of waiting threads}; void kernel_mutex_lock(struct kernel_mutex *mtx) { // Fast path: try to acquire unlocked mutex if (atomic_cmpxchg(&mtx->state, 0, 1) == 0) { return; // Got it! } // Slow path: add to wait queue spin_lock(&mtx->wait_lock); list_add_tail(¤t->wait_entry, &mtx->wait_queue); spin_unlock(&mtx->wait_lock); // Block until we're at the head and lock is released while (true) { schedule(); // Yield CPU if (/* we're at head and lock is free */) { break; } }} void kernel_mutex_unlock(struct kernel_mutex *mtx) { spin_lock(&mtx->wait_lock); if (!list_empty(&mtx->wait_queue)) { // Wake the process at the head of queue struct task_struct *next = list_first_entry(&mtx->wait_queue); wake_up_process(next); } spin_unlock(&mtx->wait_lock); atomic_set(&mtx->state, 0);} // FIFO queue ensures bounded waiting: each process in queue// enters exactly once in orderDifferent languages provide different fairness guarantees:
| Language/Primitive | Fairness Guarantee | Notes |
|---|---|---|
| Java synchronized | Not specified | Depends on JVM implementation; typically fair in modern JVMs |
| Java ReentrantLock(true) | FIFO fairness | Explicit fair mode; bounded waiting guaranteed |
| C++ std::mutex | Not specified | Implementation-defined; usually fair on modern OS |
| Python threading.Lock | FIFO (CPython) | GIL-based; effectively fair |
| Go sync.Mutex | Mixed | Combines spinning and FIFO for efficiency |
| Rust std::sync::Mutex | Not specified | Uses OS primitives; typically fair |
Advanced lock implementations like MCS and CLH locks provide bounded waiting with good scalability:
MCS Lock (Mellor-Crummey and Scott):
CLH Lock (Craig, Landin, Hagersten):
These locks achieve both bounded waiting AND high performance by avoiding contention on shared cache lines.
When bounded waiting is essential: Use explicitly fair locks (Java's ReentrantLock(true), MCS, CLH). When performance is critical and starvation is acceptable: Use simpler locks (TAS, TTAS). When in doubt: Use OS-provided primitives—they're typically fair and well-optimized.
We have completed our exploration of bounded waiting—the third and final fundamental requirement for correct synchronization solutions. Let's consolidate our understanding.
With mutual exclusion, progress, and bounded waiting, we now have a complete framework for evaluating synchronization solutions:
Mutual Exclusion: Safety — No concurrent access
Progress: Liveness — System makes forward progress
Bounded Waiting: Fairness — Every process gets its turn
A solution that satisfies all three is correct. Missing any requirement means the solution is flawed, even if it appears to work in testing.
In the next page, we will learn how to evaluate proposed solutions against these requirements systematically, building a toolkit for analyzing any synchronization algorithm.
You now understand bounded waiting—the fairness requirement that prevents starvation. You can identify algorithms that satisfy or violate this requirement, apply techniques to ensure bounded waiting, and reason about the fairness-throughput tradeoff. In the next page, we will develop systematic methods for evaluating synchronization solutions.