How does parking_lot::Condvar::wait_until differ from wait for predicate-based condition waiting?

wait_until encapsulates the predicate-checking loop that guards against spurious wakeups, replacing the manual while !predicate { condvar.wait() } pattern with a single call that atomically checks the condition and blocks only when necessary, while wait requires explicit loop-based predicate checking to be correct. This distinction matters because condition variables can wake spuriously, and wait_until ensures correct predicate evaluation without requiring developers to remember the loop pattern.

The Condition Variable Problem

use parking_lot::{Mutex, Condvar};
use std::sync::Arc;
use std::thread;
use std::time::Duration;
 
fn condition_variable_problem() {
    // Condition variables enable threads to wait for a state change
    // The classic pattern: wait for some condition to become true
    
    let pair = Arc::new((Mutex::new(false), Condvar::new()));
    let pair_clone = Arc::clone(&pair);
    
    // Spawner thread: sets the condition
    thread::spawn(move || {
        thread::sleep(Duration::from_millis(100));
        let (lock, cvar) = &*pair_clone;
        *lock.lock() = true;
        cvar.notify_one();
    });
    
    // Waiter thread: waits for condition
    let (lock, cvar) = &*pair;
    let mut started = lock.lock();
    
    // Problem: How do we wait correctly?
    // - cvar.wait() might return spuriously (without notify)
    // - Must re-check the condition after waking
    // - Must hold lock while checking
}

Condition variables require careful predicate handling; incorrect patterns lead to bugs.

Spurious Wakeups: The Hidden Danger

use parking_lot::{Mutex, Condvar};
use std::sync::Arc;
use std::thread;
 
fn spurious_wakeup_danger() {
    let pair = Arc::new((Mutex::new(0u32), Condvar::new()));
    let (lock, cvar) = &*pair;
    
    // Spurious wakeups: condvar.wait() can return WITHOUT notify
    // POSIX allows this for implementation efficiency
    // Your code MUST handle it correctly
    
    let mut counter = lock.lock();
    
    // WRONG: Using if instead of while
    // This will occasionally fail due to spurious wakeups
    if *counter < 10 {
        cvar.wait(&mut counter);
        // If we wake spuriously, *counter might still be < 10
        // But we proceed as if it's >= 10
    }
    // This is a BUG - spurious wakeups cause incorrect behavior
    
    // CORRECT: Using while loop
    while *counter < 10 {
        cvar.wait(&mut counter);
        // Re-check condition after every wakeup
        // Spurious wakeup? Loop continues, waits again
        // Real wakeup? Condition might be true, exit loop
    }
    // Now *counter >= 10 is guaranteed
}

The while loop pattern is essential for correctness because spurious wakeups are permitted.

The Basic wait Method

use parking_lot::{Mutex, Condvar};
use std::sync::Arc;
use std::thread;
use std::time::Duration;
 
fn basic_wait_pattern() {
    let data = Arc::new((Mutex::new(Vec::new()), Condvar::new()));
    
    // Producer thread
    let producer_data = Arc::clone(&data);
    thread::spawn(move || {
        let (lock, cvar) = &*producer_data;
        let mut queue = lock.lock();
        for i in 0..5 {
            queue.push(i);
            cvar.notify_one();  // Signal that data is available
            thread::sleep(Duration::from_millis(50));
        }
    });
    
    // Consumer thread using wait()
    let (lock, cvar) = &*data;
    let mut queue = lock.lock();
    
    // Manual loop pattern required with wait()
    while queue.is_empty() {
        // wait() releases the lock and blocks
        // When signaled, re-acquires lock and returns
        // We MUST loop to re-check the condition
        cvar.wait(&mut queue);
    }
    
    // Process items
    while let Some(item) = queue.pop() {
        println!("Got: {}", item);
    }
}

wait requires the caller to write the predicate loop; forgetting it causes subtle bugs.

The wait_until Method

use parking_lot::{Mutex, Condvar};
use std::sync::Arc;
use std::thread;
use std::time::Duration;
 
fn wait_until_pattern() {
    let data = Arc::new((Mutex::new(Vec::new()), Condvar::new()));
    
    // Producer thread
    let producer_data = Arc::clone(&data);
    thread::spawn(move || {
        let (lock, cvar) = &*producer_data;
        let mut queue = lock.lock();
        for i in 0..5 {
            queue.push(i);
            cvar.notify_one();
            thread::sleep(Duration::from_millis(50));
        }
    });
    
    // Consumer thread using wait_until()
    let (lock, cvar) = &*data;
    let mut queue = lock.lock();
    
    // wait_until handles the loop for us
    // Pass a predicate function that returns true when condition is met
    cvar.wait_until(&mut queue, |queue| {
        !queue.is_empty()  // Wait until queue is not empty
    });
    
    // When wait_until returns, predicate is guaranteed true
    // Queue is definitely not empty
    while let Some(item) = queue.pop() {
        println!("Got: {}", item);
    }
}

wait_until encapsulates the predicate loop, guaranteeing the condition holds when it returns.

Predicate-Based Waiting Explained

use parking_lot::{Mutex, Condvar};
use std::sync::Arc;
 
fn predicate_waiting() {
    let pair = Arc::new((Mutex::new(0u32), Condvar::new()));
    let (lock, cvar) = &*pair;
    let mut value = lock.lock();
    
    // wait_until takes a closure (predicate)
    // The closure receives the locked value
    
    // Pattern: wait_until(&mut guard, |data| condition)
    cvar.wait_until(&mut value, |v| *v >= 10);
    
    // This is equivalent to:
    while *value < 10 {
        cvar.wait(&mut value);
    }
    
    // But wait_until ensures:
    // 1. The predicate is checked before waiting
    // 2. The predicate is re-checked after every wakeup
    // 3. The function returns only when predicate is true
}

The predicate closure receives the guarded value and returns true when waiting should stop.

Implementation Comparison

use parking_lot::{Mutex, Condvar};
use std::sync::Arc;
use std::thread;
 
fn implementation_comparison() {
    let pair = Arc::new((Mutex::new(0u32), Condvar::new()));
    
    // Method 1: Manual loop with wait()
    let pair1 = Arc::clone(&pair);
    thread::spawn(move || {
        let (lock, cvar) = &*pair1;
        let mut value = lock.lock();
        
        // Manual loop - you must remember the pattern
        while *value < 10 {
            cvar.wait(&mut value);
        }
        // *value >= 10 is true
        println!("Method 1: value = {}", *value);
    });
    
    // Method 2: wait_until with predicate
    let pair2 = Arc::clone(&pair);
    thread::spawn(move || {
        let (lock, cvar) = &*pair2;
        let mut value = lock.lock();
        
        // wait_until handles the loop
        cvar.wait_until(&mut value, |v| *v >= 10);
        // *value >= 10 is guaranteed
        println!("Method 2: value = {}", *value);
    });
    
    // Both produce identical behavior
    // wait_until is less error-prone
}

Both approaches produce the same result, but wait_until is safer and more expressive.

Complex Predicates

use parking_lot::{Mutex, Condvar};
use std::sync::Arc;
use std::thread;
use std::collections::VecDeque;
 
fn complex_predicates() {
    struct SharedState {
        queue: VecDeque<String>,
        shutdown: bool,
    }
    
    let state = Arc::new((Mutex::new(SharedState {
        queue: VecDeque::new(),
        shutdown: false,
    }), Condvar::new()));
    
    // Wait for queue to have items OR shutdown
    let (lock, cvar) = &*state;
    let mut guard = lock.lock();
    
    cvar.wait_until(&mut guard, |state| {
        !state.queue.is_empty() || state.shutdown
    });
    
    // Now: either queue has items, or we're shutting down
    if guard.shutdown {
        println!("Shutting down");
    } else {
        println!("Got item: {:?}", guard.queue.pop_front());
    }
    
    // Wait for specific condition
    cvar.wait_until(&mut guard, |state| {
        state.queue.len() >= 5 || state.shutdown
    });
    
    // Wait for item matching criteria
    cvar.wait_until(&mut guard, |state| {
        state.queue.front().map(|s| s.starts_with("priority:")).unwrap_or(false)
    });
}

Complex predicates can check multiple conditions; wait_until handles them uniformly.

Timeout Support

use parking_lot::{Mutex, Condvar};
use std::sync::Arc;
use std::time::{Duration, Instant};
 
fn timeout_support() {
    let pair = Arc::new((Mutex::new(0u32), Condvar::new()));
    let (lock, cvar) = &*pair;
    let mut value = lock.lock();
    
    // wait_until with timeout
    let deadline = Instant::now() + Duration::from_secs(5);
    let result = cvar.wait_until(&mut value, deadline, |v| *v >= 10);
    
    // wait_until with timeout returns a WaitUntilResult
    // It has a .is_success() method
    if result.is_success() {
        println!("Condition met: value = {}", *value);
    } else {
        println!("Timeout elapsed, value = {}", *value);
    }
    
    // Equivalent manual pattern with wait_timeout:
    let deadline = Instant::now() + Duration::from_secs(5);
    while *value < 10 {
        let timeout_result = cvar.wait_until(&mut value, deadline);
        if timeout_result.timed_out() {
            break;
        }
    }
    
    // The wait_until version is cleaner and harder to get wrong
}

wait_until with deadlines combines predicate checking with timeout handling.

Return Value of wait_until

use parking_lot::{Mutex, Condvar};
use std::sync::Arc;
use std::time::{Duration, Instant};
 
fn wait_until_return_value() {
    let pair = Arc::new((Mutex::new(0u32), Condvar::new()));
    let (lock, cvar) = &*pair;
    let mut value = lock.lock();
    
    // Without timeout: returns MutexGuard
    // The guard is returned when predicate becomes true
    cvar.wait_until(&mut value, |v| *v >= 10);
    // value is still locked and *value >= 10
    
    // With timeout: returns WaitUntilResult
    let deadline = Instant::now() + Duration::from_secs(5);
    let result = cvar.wait_until(&mut value, deadline, |v| *v >= 10);
    
    // Check if condition was met or timed out
    if result.is_success() {
        // Condition met within timeout
        // *value >= 10 is guaranteed
    } else {
        // Timed out
        // *value might be anything (< 10, or >= 10 if condition became true)
        // Check the actual value
    }
    
    // The MutexGuard is still held in both cases
    // value remains locked
}

wait_until returns the guard (or result with guard access), maintaining lock ownership semantics.

Common Mistakes with wait()

use parking_lot::{Mutex, Condvar};
use std::sync::Arc;
 
fn common_mistakes() {
    let pair = Arc::new((Mutex::new(0u32), Condvar::new()));
    let (lock, cvar) = &*pair;
    
    // MISTAKE 1: Using if instead of while
    {
        let mut value = lock.lock();
        if *value < 10 {  // WRONG: should be while
            cvar.wait(&mut value);
            // Spurious wakeup can reach here even if value < 10
        }
        // *value >= 10 is NOT guaranteed
    }
    
    // MISTAKE 2: Checking condition before lock
    {
        // WRONG: Checking without lock
        // if *value < 10 { ... }  // value isn't locked!
        
        let mut value = lock.lock();
        while *value < 10 {
            cvar.wait(&mut value);
        }
    }
    
    // MISTAKE 3: Modifying condition without notify
    {
        let mut value = lock.lock();
        *value = 20;  // Changed condition
        // Forgot cvar.notify_one() or notify_all()!
        // Waiting threads may never wake
    }
    
    // wait_until prevents mistake 1 by encapsulating the loop
    {
        let mut value = lock.lock();
        cvar.wait_until(&mut value, |v| *v >= 10);
        // Guaranteed: *value >= 10
        // No spurious wakeup bug possible
    }
}

wait_until eliminates the most common mistake: forgetting the while loop.

Performance Considerations

use parking_lot::{Mutex, Condvar};
use std::sync::Arc;
 
fn performance_notes() {
    // wait_until is NOT slower than manual loop
    // Both compile to essentially the same code
    
    let pair = Arc::new((Mutex::new(0u32), Condvar::new()));
    let (lock, cvar) = &*pair;
    let mut value = lock.lock();
    
    // Manual loop (what wait_until does internally):
    while *value < 10 {
        cvar.wait(&mut value);
    }
    
    // wait_until equivalent:
    cvar.wait_until(&mut value, |v| *v >= 10);
    
    // Performance characteristics:
    // - Same number of wakeups and lock acquisitions
    // - Same spurious wakeup handling
    // - Minimal overhead from closure call (often inlined)
    
    // The closure is called:
    // 1. Before waiting (to check if already true)
    // 2. After each wakeup (to check if condition met)
    // Same as manual loop, just cleaner syntax
    
    // For very hot paths, the closure overhead exists but is tiny
    // Usually negligible compared to lock contention
}

wait_until has negligible overhead compared to manual loops; clarity wins.

Producer-Consumer Pattern

use parking_lot::{Mutex, Condvar};
use std::sync::Arc;
use std::thread;
use std::collections::VecDeque;
 
fn producer_consumer() {
    struct Queue {
        items: VecDeque<u32>,
        closed: bool,
    }
    
    let state = Arc::new((Mutex::new(Queue { items: VecDeque::new(), closed: false }), Condvar::new()));
    
    // Producer
    let producer_state = Arc::clone(&state);
    let producer = thread::spawn(move || {
        let (lock, cvar) = &*producer_state;
        let mut queue = lock.lock();
        
        for i in 0..10 {
            queue.items.push_back(i);
            cvar.notify_one();  // Wake one waiting consumer
        }
        
        queue.closed = true;
        cvar.notify_all();  // Wake all consumers to check closed flag
    });
    
    // Consumer using wait_until
    let consumer_state = Arc::clone(&state);
    let consumer = thread::spawn(move || {
        let (lock, cvar) = &*consumer_state;
        let mut queue = lock.lock();
        
        loop {
            // Wait for items or closed
            cvar.wait_until(&mut queue, |q| !q.items.is_empty() || q.closed);
            
            // Process all available items
            while let Some(item) = queue.items.pop_front() {
                println!("Consumed: {}", item);
            }
            
            if queue.closed {
                break;
            }
        }
    });
    
    producer.join().unwrap();
    consumer.join().unwrap();
}

The producer-consumer pattern benefits from wait_until's clear predicate expression.

Thread Pool Shutdown Pattern

use parking_lot::{Mutex, Condvar};
use std::sync::Arc;
use std::thread;
 
fn thread_pool_shutdown() {
    struct PoolState {
        active_jobs: usize,
        shutdown: bool,
    }
    
    let state = Arc::new((Mutex::new(PoolState { active_jobs: 0, shutdown: false }), Condvar::new()));
    
    // Worker threads
    let workers: Vec<_> = (0..4)
        .map(|id| {
            let worker_state = Arc::clone(&state);
            thread::spawn(move || {
                let (lock, cvar) = &*worker_state;
                let mut state = lock.lock();
                
                loop {
                    // Wait for work or shutdown
                    cvar.wait_until(&mut state, |s| s.shutdown || s.active_jobs > 0);
                    
                    if state.shutdown && state.active_jobs == 0 {
                        break;  // Exit thread
                    }
                    
                    if state.active_jobs > 0 {
                        state.active_jobs -= 1;
                        drop(state);  // Release lock while working
                        
                        // Do work...
                        println!("Worker {} processing", id);
                        
                        state = lock.lock();  // Re-acquire
                    }
                }
            })
        })
        .collect();
    
    // Shutdown: wait for all jobs to complete
    let (lock, cvar) = &*state;
    let mut state = lock.lock();
    state.shutdown = true;
    cvar.notify_all();  // Wake all workers
    
    // Wait for all jobs to finish
    cvar.wait_until(&mut state, |s| s.active_jobs == 0);
    
    for worker in workers {
        worker.join().unwrap();
    }
}

Shutdown patterns use wait_until to wait for complex state conditions safely.

Bounded Buffer Pattern

use parking_lot::{Mutex, Condvar};
use std::sync::Arc;
use std::collections::VecDeque;
 
fn bounded_buffer() {
    struct Buffer<T> {
        items: VecDeque<T>,
        capacity: usize,
    }
    
    let buffer = Arc::new((
        Mutex::new(Buffer { items: VecDeque::new(), capacity: 5 }),
        Condvar::new(),
        Condvar::new(),  // Two condvars: not_empty and not_full
    ));
    
    let (lock, not_empty, not_full) = &*buffer;
    
    // Producer: wait until not full
    {
        let mut buf = lock.lock();
        not_full.wait_until(&mut buf, |b| b.items.len() < b.capacity);
        buf.items.push_back(42);
        not_empty.notify_one();  // Signal consumers
    }
    
    // Consumer: wait until not empty
    {
        let mut buf = lock.lock();
        not_empty.wait_until(&mut buf, |b| !b.items.is_empty());
        let item = buf.items.pop_front();
        not_full.notify_one();  // Signal producers
    }
}

Bounded buffers use wait_until to wait for space or data availability.

Comparing std::sync vs parking_lot

use parking_lot::{Mutex, Condvar};
use std::sync::{Mutex as StdMutex, Condvar as StdCondvar};
use std::sync::Arc;
 
fn comparing_implementations() {
    // std::sync version (requires MutexGuard)
    let std_pair = Arc::new((StdMutex::new(0u32), StdCondvar::new()));
    {
        let (lock, cvar) = &*std_pair;
        let mut value = lock.lock().unwrap();
        
        // std::sync::Condvar::wait takes MutexGuard
        while *value < 10 {
            value = cvar.wait(value).unwrap();
        }
        
        // std::sync::Condvar::wait_while (equivalent to parking_lot's wait_until)
        value = cvar.wait_while(value, |v| *v < 10).unwrap();
    }
    
    // parking_lot version (requires MutexGuard)
    let pl_pair = Arc::new((Mutex::new(0u32), Condvar::new()));
    {
        let (lock, cvar) = &*pl_pair;
        let mut value = lock.lock();
        
        // parking_lot::Condvar::wait
        while *value < 10 {
            cvar.wait(&mut value);
        }
        
        // parking_lot::Condvar::wait_until
        cvar.wait_until(&mut value, |v| *v >= 10);
    }
    
    // Key differences:
    // 1. parking_lot doesn't need unwrap() (no poisoning)
    // 2. parking_lot takes &mut MutexGuard, std takes ownership
    // 3. parking_lot's wait_until is cleaner than std's wait_while
    // 4. parking_lot generally has better performance
}

parking_lot provides cleaner API and better performance than std::sync.

Complete Example: Job Queue

use parking_lot::{Mutex, Condvar};
use std::sync::Arc;
use std::thread;
use std::collections::VecDeque;
use std::time::Duration;
 
fn complete_job_queue_example() {
    struct JobQueue {
        jobs: VecDeque<String>,
        shutdown: bool,
        active_workers: usize,
    }
    
    let state = Arc::new((
        Mutex::new(JobQueue {
            jobs: VecDeque::new(),
            shutdown: false,
            active_workers: 0,
        }),
        Condvar::new(),
    ));
    
    // Worker function
    fn worker(id: usize, state: Arc<(Mutex<JobQueue>, Condvar)>) {
        let (lock, cvar) = &*state;
        let mut queue = lock.lock();
        
        loop {
            // Wait for: jobs available OR shutdown with no active work
            cvar.wait_until(&mut queue, |q| {
                !q.jobs.is_empty() || (q.shutdown && q.active_workers == 0)
            });
            
            // Check shutdown condition
            if queue.shutdown && queue.jobs.is_empty() {
                break;
            }
            
            // Get job
            if let Some(job) = queue.jobs.pop_front() {
                queue.active_workers += 1;
                drop(queue);  // Release lock while working
                
                println!("Worker {} processing: {}", id, job);
                thread::sleep(Duration::from_millis(100));  // Simulate work
                
                queue = lock.lock();
                queue.active_workers -= 1;
                cvar.notify_all();  // Notify that active_workers changed
            }
        }
    }
    
    // Spawn workers
    let workers: Vec<_> = (0..4)
        .map(|id| {
            let state = Arc::clone(&state);
            thread::spawn(move || worker(id, state))
        })
        .collect();
    
    // Add jobs
    {
        let (lock, cvar) = &*state;
        let mut queue = lock.lock();
        for i in 0..10 {
            queue.jobs.push_back(format!("Job {}", i));
        }
        cvar.notify_all();  // Wake all workers
    }
    
    // Shutdown
    thread::sleep(Duration::from_secs(2));
    {
        let (lock, cvar) = &*state;
        let mut queue = lock.lock();
        queue.shutdown = true;
        cvar.notify_all();  // Wake workers to check shutdown
    }
    
    // Wait for workers
    for worker in workers {
        worker.join().unwrap();
    }
    
    println!("All workers finished");
}

A complete job queue using wait_until for clean condition handling.

Summary Table

fn summary() {
    // | Method          | Pattern                        | Returns         |
    // |-----------------|--------------------------------|-----------------|
    // | wait            | Manual while loop required     | ()              |
    // | wait_until      | Predicate closure, auto-loop   | MutexGuard      |
    // | wait_timeout    | Manual loop + timeout check    | WaitTimeoutResult |
    // | wait_until(deadline, pred) | Predicate + timeout | WaitUntilResult |
    
    // | Feature                  | wait()     | wait_until() |
    // |--------------------------|------------|--------------|
    // | Spurious wakeup safe     | Manual     | Automatic    |
    // | Predicate required       | No         | Yes          |
    // | Timeout support          | wait_timeout | wait_until(deadline, pred) |
    // | Common bug prevention    | No         | Yes          |
    // | Code clarity             | Lower      | Higher       |
}

Synthesis

Quick reference:

use parking_lot::{Mutex, Condvar};
 
let pair = Arc::new((Mutex::new(0u32), Condvar::new()));
let (lock, cvar) = &*pair;
let mut value = lock.lock();
 
// Manual pattern with wait()
while *value < 10 {
    cvar.wait(&mut value);
}
 
// Equivalent with wait_until()
cvar.wait_until(&mut value, |v| *v >= 10);
 
// With timeout
use std::time::{Duration, Instant};
let deadline = Instant::now() + Duration::from_secs(5);
let result = cvar.wait_until(&mut value, deadline, |v| *v >= 10);
if result.is_success() {
    // Condition met within timeout
}

Key insight: wait_until exists because condition variable programming is error-prone—the while !predicate { wait() } pattern must be used to guard against spurious wakeups, but it's easy to forget or write incorrectly as if. wait_until encapsulates this pattern, taking a predicate closure that returns true when waiting should stop. The implementation is simple: check the predicate, if false then wait and re-check. This guarantees the predicate holds when wait_until returns, eliminating a whole class of bugs. The method also composes naturally with timeouts, returning a result that indicates whether the condition was met or time elapsed. Use wait when you need fine-grained control over the loop (complex state transitions, multiple conditions that change independently), but default to wait_until for clarity and correctness—the performance is identical, but the safety is higher.