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 behaviorBoth 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.
