How does parking_lot::Condvar differ from std::sync::Condvar regarding spurious wakeups?

parking_lot::Condvar provides stronger practical guarantees against spurious wakeups than std::sync::Condvar—while both APIs technically allow spurious wakeups per their specifications, parking_lot's implementation is designed to only wake waiting threads when explicitly signaled, whereas std::sync::Condvar may return from wait due to implementation details like lock contention or OS-level thread management. This means code using parking_lot::Condvar encounters fewer unexpected wakeups in practice, though correct code must still handle them.

Standard Condvar Wait Pattern

use std::sync::{Mutex, Condvar};
use std::thread;
 
fn std_condvar_basic() {
    let pair = (Mutex::new(false), Condvar::new());
    
    thread::spawn(move || {
        let (lock, cvar) = &pair;
        
        // Wait for condition
        let mut started = lock.lock().unwrap();
        while !*started {
            // wait() releases the lock and blocks
            // When it returns, lock is re-acquired
            started = cvar.wait(started).unwrap();
        }
        // Condition is now true
    });
    
    let (lock, cvar) = &pair;
    let mut started = lock.lock().unwrap();
    *started = true;
    cvar.notify_one();
}

Both condvars use the same basic wait/signal pattern with a mutex and condition variable.

Spurious Wakeup Demonstration

use std::sync::{Mutex, Condvar};
use std::thread;
use std::sync::atomic::{AtomicUsize, Ordering};
 
fn demonstrate_spurious_wakeup() {
    let pair = (Mutex::new(false), Condvar::new());
    let wakeup_count = AtomicUsize::new(0);
    
    let handle = thread::spawn(|| {
        let (lock, cvar) = &pair;
        let mut guard = lock.lock().unwrap();
        
        // Condition starts false
        // wait() might return spuriously even without notify
        while !*guard {
            guard = cvar.wait(guard).unwrap();
            wakeup_count.fetch_add(1, Ordering::SeqCst);
            // If this was spurious, *guard is still false
            // Loop continues and waits again
        }
    });
    
    // Never call notify - but wait might still return!
    // In std::sync::Condvar, this can happen due to:
    // - OS scheduler decisions
    // - Signal handling
    // - Implementation details
    
    handle.join().unwrap();
}

Spurious wakeups mean wait() can return without notify_* being called.

The Loop Pattern for Both

use std::sync::{Mutex, Condvar};
 
fn wait_loop_pattern() {
    let pair = (Mutex::new(0u32), Condvar::new());
    
    // CORRECT: Always use while loop with condition check
    fn wait_for_value(pair: &(Mutex<u32>, Condvar), target: u32) {
        let (lock, cvar) = pair;
        let mut guard = lock.lock().unwrap();
        
        while *guard != target {
            guard = cvar.wait(guard).unwrap();
        }
        // Now *guard == target is guaranteed
    }
    
    // INCORRECT: if statement is wrong
    fn wait_for_value_wrong(pair: &(Mutex<u32>, Condvar), target: u32) {
        let (lock, cvar) = pair;
        let mut guard = lock.lock().unwrap();
        
        // WRONG: Spurious wakeup could cause premature return
        if *guard != target {
            guard = cvar.wait(guard).unwrap();
            // Here *guard might still not equal target!
        }
        // *guard == target is NOT guaranteed
    }
}

Both std::sync::Condvar and parking_lot::Condvar require the while-loop pattern for correctness.

parking_lot Condvar

use parking_lot::{Mutex, Condvar};
use std::thread;
 
fn parking_lot_condvar() {
    let pair = (Mutex::new(false), Condvar::new());
    
    thread::spawn(move || {
        let (lock, cvar) = &pair;
        let mut started = lock.lock();
        
        while !*started {
            cvar.wait(&mut started);
        }
    });
    
    let (lock, cvar) = &pair;
    let mut started = lock.lock();
    *started = true;
    cvar.notify_one();
}

parking_lot::Condvar has a similar API but different implementation guarantees.

Implementation Differences

use std::sync::{Mutex as StdMutex, Condvar as StdCondvar};
use parking_lot::{Mutex as PlMutex, Condvar as PlCondvar};
 
fn implementation_differences() {
    // std::sync::Condvar:
    // - Built on OS primitives (pthread_condvar on Unix, CONDITION_VARIABLE on Windows)
    // - Subject to OS-level spurious wakeups
    // - POSIX explicitly permits spurious wakeups
    // - Windows CONDITION_VARIABLE can have implementation-specific wakeups
    
    // parking_lot::Condvar:
    // - Uses parking_lot's own thread parking mechanism
    // - Wakes only on explicit notify_one/notify_all
    // - Much fewer spurious wakeups in practice
    // - Still technically permitted by API
    
    // The key difference is control:
    // std delegates to OS, parking_lot implements its own queue
}

parking_lot uses its own implementation rather than OS primitives.

Practical Guarantees

use parking_lot::{Mutex, Condvar};
use std::thread;
use std::sync::atomic::{AtomicUsize, Ordering};
 
fn practical_behavior() {
    let pair = (Mutex::new(0u32), Condvar::new());
    let spurious_count = AtomicUsize::new(0);
    
    let handle = thread::spawn(|| {
        let (lock, cvar) = &pair;
        let mut guard = lock.lock();
        
        for _ in 0..1000 {
            // Wait without condition check to count wakeups
            cvar.wait(&mut guard);
            spurious_count.fetch_add(1, Ordering::SeqCst);
        }
    });
    
    // With std::sync::Condvar, wait() might return multiple times
    // without any notify calls, due to OS behavior
    
    // With parking_lot::Condvar, wait() returns ONLY when:
    // - notify_one() or notify_all() is called
    // - (with very rare edge cases)
    
    // In practice, parking_lot::Condvar has far fewer spurious wakeups
    // But the API still requires the while-loop pattern
    
    handle.join().unwrap();
}

parking_lot::Condvar rarely has spurious wakeups, but code must still handle them.

wait_while and wait_until

use parking_lot::{Mutex, Condvar};
 
fn convenience_methods() {
    let pair = (Mutex::new(0u32), Condvar::new());
    let (lock, cvar) = &pair;
    let mut guard = lock.lock();
    
    // Manual loop pattern (required for std)
    while *guard < 10 {
        guard = cvar.wait(guard);
    }
    
    // parking_lot provides convenience methods that handle the loop
    // wait_while: Wait while predicate is true
    cvar.wait_while(&mut guard, |v| *v < 10);
    
    // wait_until: Wait until predicate is true (equivalent)
    cvar.wait_until(&mut guard, |v| *v >= 10);
    
    // These methods internally use the while-loop pattern
    // They handle spurious wakeups automatically
    
    // std::sync::Condvar does NOT have these convenience methods
    // (as of Rust stable - they may be added in future)
}

parking_lot::Condvar provides wait_while/wait_until that encapsulate the loop pattern.

Timeout Variants

use std::time::Duration;
use parking_lot::{Mutex, Condvar};
 
fn timeout_variants() {
    let pair = (Mutex::new(0u32), Condvar::new());
    let (lock, cvar) = &pair;
    let mut guard = lock.lock();
    
    // wait_timeout returns (MutexGuard, WaitTimeoutResult)
    let result = cvar.wait_timeout(&mut guard, Duration::from_secs(1));
    // result.1.timed_out() indicates if timeout occurred
    
    // wait_timeout_ms uses milliseconds
    let result = cvar.wait_timeout_ms(&mut guard, 1000);
    
    // wait_timeout_until combines timeout with predicate
    let result = cvar.wait_timeout_until(&mut guard, Duration::from_secs(1), |v| *v > 0);
    // Returns true if predicate satisfied, false if timed out
    
    // wait_timeout_while is the opposite
    let result = cvar.wait_timeout_while(&mut guard, Duration::from_secs(1), |v| *v == 0);
    
    // All timeout variants handle spurious wakeups internally
}

Timeout variants also handle spurious wakeups internally.

std::sync::Condvar Timeout

use std::sync::{Mutex, Condvar};
use std::time::Duration;
 
fn std_timeout() {
    let pair = (Mutex::new(0u32), Condvar::new());
    let (lock, cvar) = &pair;
    let mut guard = lock.lock().unwrap();
    
    // std::sync::Condvar has wait_timeout
    let result = cvar.wait_timeout(guard, Duration::from_secs(1)).unwrap();
    
    // result.0 is the MutexGuard
    // result.1 is WaitTimeoutResult with .timed_out() method
    
    if result.1.timed_out() {
        println!("Timed out");
    } else {
        println!("Woke up before timeout (possibly spurious)");
    }
    
    // Must still check condition in loop for correctness
    let mut started = lock.lock().unwrap();
    let result = loop {
        let r = cvar.wait_timeout(started, Duration::from_millis(100)).unwrap();
        if *r.0 {
            break r;
        }
        if r.1.timed_out() {
            break r;
        }
        started = r.0;
    };
}

std::sync::Condvar requires manual loop even with timeouts.

Wakeup Sources in std

use std::sync::{Mutex, Condvar};
 
fn wakeup_sources_std() {
    // std::sync::Condvar can wake up from:
    
    // 1. Explicit signal: notify_one() or notify_all()
    //    - This is the intended wakeup source
    
    // 2. OS-level spurious wakeup:
    //    - POSIX pthread_cond_wait permits spurious wakeups
    //    - Can happen due to scheduler decisions
    //    - Can happen during signal handling
    //    - Frequency varies by OS implementation
    
    // 3. Thread cancellation/cleanup (rare in Rust)
    
    // 4. Implementation-specific behaviors
    //    - Some OSes wake threads on lock contention resolution
    //    - Virtualization can affect behavior
    
    // The POSIX rationale for allowing spurious wakeups:
    // - Allows more efficient implementations
    // - Easier to implement on some platforms
    // - Avoids some edge cases in signal handling
}

std::sync::Condvar inherits OS-level spurious wakeups.

Wakeup Sources in parking_lot

use parking_lot::{Mutex, Condvar};
 
fn wakeup_sources_parking_lot() {
    // parking_lot::Condvar wakes up from:
    
    // 1. Explicit signal: notify_one() or notify_all()
    //    - This is the primary wakeup source
    
    // 2. Very rare edge cases:
    //    - Thread unparking for other reasons (extremely rare)
    //    - Implementation may change behavior
    
    // Unlike std, parking_lot:
    // - Uses its own thread parking mechanism
    // - Maintains explicit wait queue
    // - Only unparks threads when notified
    
    // The queue-based implementation means:
    // - No OS-level spurious wakeups
    // - Predictable wakeup behavior
    // - Still MUST use while-loop for correctness
}

parking_lot::Condvar uses its own queue, avoiding OS spurious wakeups.

Wait Queue Behavior

use parking_lot::{Mutex, Condvar};
use std::thread;
use std::sync::Arc;
 
fn wait_queue() {
    let pair = Arc::new((Mutex::new(false), Condvar::new()));
    
    // parking_lot maintains an explicit queue of waiting threads
    // notify_one wakes the first thread in the queue
    // notify_all wakes all threads in the queue
    
    // std::sync::Condvar uses OS primitives
    // On Linux with pthreads, this is futex-based
    // On Windows, uses CONDITION_VARIABLE
    // Wake order depends on OS scheduler
    
    let pair1 = Arc::clone(&pair);
    let h1 = thread::spawn(move || {
        let (lock, cvar) = &*pair1;
        let mut guard = lock.lock();
        while !*guard {
            cvar.wait(&mut guard);
        }
    });
    
    let pair2 = Arc::clone(&pair);
    let h2 = thread::spawn(move || {
        let (lock, cvar) = &*pair2;
        let mut guard = lock.lock();
        while !*guard {
            cvar.wait(&mut guard);
        }
    });
    
    // Both threads are now in parking_lot's wait queue
    // They will only wake on notify_* (in practice)
}

parking_lot maintains an explicit wait queue; std uses OS primitives.

Deadlock Scenarios

use std::sync::{Mutex as StdMutex, Condvar as StdCondvar};
use parking_lot::{Mutex as PlMutex, Condvar as PlCondvar};
 
fn deadlock_considerations() {
    // Both condvars have similar deadlock patterns:
    
    // 1. Forgot to notify
    // Thread waits forever - same in both
    
    // 2. Condition never becomes true
    // Thread waits forever - same in both
    
    // 3. Lost wakeup
    // Thread misses signal - same in both
    
    // 4. Mutex not held during wait
    // Undefined behavior - same in both
    
    // The while-loop pattern prevents correctness issues
    // in both implementations
    
    // Spurious wakeups don't cause deadlocks
    // They just make wait return early
    // The loop handles this by rechecking condition
}

Both condvars have similar correctness requirements.

Fair Wakeups

use parking_lot::{Mutex, Condvar};
 
fn fair_wakeups() {
    // parking_lot::Condvar uses a fair queue
    // Threads are woken in FIFO order
    
    // This means:
    // - First thread to wait is first to wake
    // - notify_one wakes the oldest waiter
    // - Predictable ordering
    
    // std::sync::Condvar behavior:
    // - Wake order depends on OS scheduler
    // - May not be FIFO
    // - "Thundering herd" possible on notify_all
    
    // Fair wakeups can be important for:
    // - Priority inversion avoidance
    // - Predictable latency
    // - Fairness guarantees
}

parking_lot::Condvar provides FIFO wake ordering.

Performance Characteristics

use std::sync::{Mutex as StdMutex, Condvar as StdCondvar};
use parking_lot::{Mutex as PlMutex, Condvar as PlCondvar};
 
fn performance() {
    // std::sync::Condvar:
    // - Uses OS syscalls for wait/wake
    // - Heavier weight on contended paths
    // - OS may batch wakeups
    // - May have better throughput on some systems
    
    // parking_lot::Condvar:
    // - Uses userspace parking mechanism
    // - Fewer syscalls in uncontended case
    // - May be faster for low-contention scenarios
    // - More consistent latency
    
    // Both are O(1) for wait/wake operations
    // parking_lot::Mutex + Condvar often faster together
    // because they share underlying parking infrastructure
    
    // For high-contention scenarios:
    // - Performance depends on OS and hardware
    // - std may benefit from OS optimization
    // - parking_lot may have more predictable behavior
}

Performance differs based on contention patterns and OS.

Comparison Summary

fn comparison_table() {
    // | Aspect | std::sync::Condvar | parking_lot::Condvar |
    // |--------|-------------------|----------------------|
    // | Implementation | OS primitives | Custom parking queue |
    // | Spurious wakeups | Common (OS-level) | Rare (implementation) |
    // | API requirement | while-loop mandatory | while-loop mandatory |
    // | Convenience methods | Manual loop | wait_while/wait_until |
    // | Wake order | OS-dependent | FIFO (fair) |
    // | Syscalls | Per wait/wake | Batched (parking) |
    // | Lock type | Mutex | parking_lot::Mutex |
    // | Lock guard | MutexGuard | MutexGuard (Send!) |
}

The key difference is implementation and spurious wakeup frequency.

Migration Between Implementations

use std::sync::{Mutex, Condvar};
// use parking_lot::{Mutex, Condvar};
 
fn migration_pattern() {
    // std::sync::Condvar pattern
    let pair = (Mutex::new(false), Condvar::new());
    {
        let (lock, cvar) = &pair;
        let mut guard = lock.lock().unwrap();  // unwrap for std
        while !*guard {
            guard = cvar.wait(guard).unwrap();  // unwrap for std
        }
    }
    
    // parking_lot::Condvar pattern
    // let pair = (Mutex::new(false), Condvar::new());
    // {
    //     let (lock, cvar) = &pair;
    //     let mut guard = lock.lock();  // no unwrap needed
    //     while !*guard {
    //         cvar.wait(&mut guard);  // no unwrap needed
    //     }
    //     
    //     // Or use convenience method:
    //     cvar.wait_while(&mut guard, |g| !*g);
    // }
    
    // Key differences in migration:
    // 1. No Result handling (parking_lot doesn't use Result)
    // 2. wait takes &mut guard instead of owning and returning
    // 3. wait_while/wait_until convenience methods available
}

Migration requires adjusting to different API conventions.

Why the While Loop is Still Required

use parking_lot::{Mutex, Condvar};
 
fn why_loop_still_required() {
    // Even with parking_lot::Condvar's stronger guarantees,
    // the while-loop pattern is required because:
    
    // 1. API contract: The API allows spurious wakeups
    //    - Future versions might change implementation
    //    - Code must be correct per specification
    
    // 2. Condition state: The condition might still be false
    //    - Another thread acquired lock first
    //    - Condition was temporarily true, then changed
    
    // 3. Thread safety: The loop pattern is idiomatic
    //    - Clearer intent
    //    - Works with both implementations
    
    let pair = (Mutex::new(0u32), Condvar::new());
    let (lock, cvar) = &pair;
    let mut guard = lock.lock();
    
    // This is always correct:
    cvar.wait_while(&mut guard, |v| *v < 10);
    
    // Equivalent to:
    while *guard < 10 {
        cvar.wait(&mut guard);
    }
}

Correct code uses the while-loop regardless of implementation.

Synthesis

Spurious wakeup comparison:

Source std::sync::Condvar parking_lot::Condvar
Explicit notify Yes Yes
OS-level spurious Yes (frequent) No (uses parking)
Implementation spurious Rare Very rare
Practical frequency Moderate Minimal

Key insight: The fundamental difference is that std::sync::Condvar delegates to OS primitives (pthreads on Unix, CONDITION_VARIABLE on Windows) which explicitly permit spurious wakeups for implementation flexibility. parking_lot::Condvar implements its own wait queue on top of the parking mechanism, giving it precise control over when threads wake—the implementation only unparks threads when notify_one or notify_all is called. However, both APIs technically permit spurious wakeups in their specification, so correct code must always use the while-loop pattern (or wait_while/wait_until convenience methods in parking_lot).

When to use which:

// Use std::sync::Condvar when:
// - Working with existing std::sync::Mutex
// - Need std::sync compatibility
// - OS-optimized primitives might be faster
// - Part of std library, no dependencies
 
// Use parking_lot::Condvar when:
// - Using parking_lot::Mutex already
// - Want fewer spurious wakeups
// - Need FIFO wake ordering
// - Prefer wait_while/wait_until convenience methods
// - Want consistent cross-platform behavior

Both are correct and thread-safe—the choice is about API preference, ecosystem consistency, and practical behavior, not about fundamental correctness since both require the same loop pattern for proper usage.