How does parking_lot::Condvar compare to std::sync::Condvar in terms of API ergonomics?

parking_lot::Condvar provides a more ergonomic API that avoids the common pitfalls of std::sync::Condvar by not requiring a mutex guard to be passed to wait operations, using simpler timeout handling, and eliminating spurious wakeups in practice. The standard library's Condvar requires the MutexGuard to be passed to wait() so it can atomically unlock and wait, while parking_lot::Condvar simply unlocks the Mutex directly and doesn't require holding a guard. This design difference reduces boilerplate, prevents incorrect usage patterns, and makes the API more approachable while maintaining the same fundamental synchronization guarantees.

Standard Library Condvar Pattern

use std::sync::{Arc, Mutex, Condvar};
use std::thread;
 
fn main() {
    let pair = Arc::new((Mutex::new(false), Condvar::new()));
    let pair_clone = Arc::clone(&pair);
    
    thread::spawn(move || {
        let (lock, cvar) = &*pair_clone;
        let mut started = lock.lock().unwrap();
        *started = true;
        cvar.notify_one();  // Notify waiting thread
    });
    
    let (lock, cvar) = &*pair;
    let mut started = lock.lock().unwrap();
    
    // Must pass the guard to wait()
    while !*started {
        started = cvar.wait(started).unwrap();
    }
    
    println!("Thread has started");
}

std::sync::Condvar::wait() requires the mutex guard as a parameter.

Parking_lot Condvar Pattern

use parking_lot::{Mutex, Condvar};
use std::sync::Arc;
use std::thread;
 
fn main() {
    let pair = Arc::new((Mutex::new(false), Condvar::new()));
    let pair_clone = Arc::clone(&pair);
    
    thread::spawn(move || {
        let (lock, cvar) = &*pair_clone;
        let mut started = lock.lock();
        *started = true;
        cvar.notify_one();  // Notify waiting thread
    });
    
    let (lock, cvar) = &*pair;
    let mut started = lock.lock();
    
    // No guard parameter needed
    while !*started {
        cvar.wait(&mut started);
    }
    
    println!("Thread has started");
}

parking_lot::Condvar::wait() takes a mutable reference to the guard instead.

The Guard Parameter Difference

use std::sync::{Mutex, Condvar};
 
fn std_pattern() {
    let mutex = Mutex::new(42);
    let cvar = Condvar::new();
    
    let guard = mutex.lock().unwrap();
    
    // wait() takes ownership of the guard
    // and returns it when signaled
    let guard = cvar.wait(guard).unwrap();
    
    // The pattern requires re-binding because wait() returns the guard
}

std::sync::Condvar::wait() consumes and returns the guard.

use parking_lot::{Mutex, Condvar};
 
fn parking_lot_pattern() {
    let mutex = Mutex::new(42);
    let cvar = Condvar::new();
    
    let mut guard = mutex.lock();
    
    // wait() takes a reference
    cvar.wait(&mut guard);
    
    // Guard is still valid, no re-binding needed
}

parking_lot::Condvar::wait() borrows the guard mutably.

Wait with Timeout

use std::sync::{Mutex, Condvar};
use std::time::Duration;
 
fn std_timeout() {
    let mutex = Mutex::new(false);
    let cvar = Condvar::new();
    
    let guard = mutex.lock().unwrap();
    
    // wait_timeout() returns (MutexGuard, WaitTimeoutResult)
    let (guard, result) = cvar.wait_timeout(guard, Duration::from_secs(1)).unwrap();
    
    if result.timed_out() {
        println!("Timed out");
    }
    
    // Guard is available again
    let _value = *guard;
}

std::sync::Condvar::wait_timeout() returns a tuple of the guard and a timeout result.

use parking_lot::{Mutex, Condvar};
use std::time::Duration;
 
fn parking_lot_timeout() {
    let mutex = Mutex::new(false);
    let cvar = Condvar::new();
    
    let mut guard = mutex.lock();
    
    // wait_timeout() returns bool for timeout status
    let timed_out = cvar.wait_timeout(&mut guard, Duration::from_secs(1));
    
    if timed_out {
        println!("Timed out");
    }
    
    // Guard is still valid
    let _value = *guard;
}

parking_lot::Condvar::wait_timeout() returns a simple bool.

Multiple Wait Variants

use std::sync::{Mutex, Condvar};
use std::time::{Duration, Instant};
 
fn std_variants() {
    let mutex = Mutex::new(0);
    let cvar = Condvar::new();
    let guard = mutex.lock().unwrap();
    
    // Basic wait
    let guard = cvar.wait(guard).unwrap();
    
    // Wait with duration timeout
    let (guard, result) = cvar.wait_timeout(guard, Duration::from_secs(1)).unwrap();
    
    // Wait with instant deadline
    let deadline = Instant::now() + Duration::from_secs(1);
    let (guard, result) = cvar.wait_timeout_until(guard, deadline).unwrap();
}

std::sync::Condvar provides three wait variants.

use parking_lot::{Mutex, Condvar};
use std::time::{Duration, Instant};
 
fn parking_lot_variants() {
    let mutex = Mutex::new(0);
    let cvar = Condvar::new();
    let mut guard = mutex.lock();
    
    // Basic wait
    cvar.wait(&mut guard);
    
    // Wait with duration
    let timed_out = cvar.wait_timeout(&mut guard, Duration::from_secs(1));
    
    // Wait until deadline
    let deadline = Instant::now() + Duration::from_secs(1);
    let timed_out = cvar.wait_until(&mut guard, deadline);
}

parking_lot::Condvar uses consistent reference-based API for all variants.

Wait While Predicate

use std::sync::{Mutex, Condvar};
 
fn std_wait_while() {
    let mutex = Mutex::new(0);
    let cvar = Condvar::new();
    let guard = mutex.lock().unwrap();
    
    // wait_while() waits while predicate returns true
    let guard = cvar.wait_while(guard, |value| *value < 10).unwrap();
    
    // Guard now has value >= 10
    println!("Value: {}", *guard);
}

std::sync::Condvar::wait_while() simplifies the common wait-loop pattern.

use parking_lot::{Mutex, Condvar};
 
fn parking_lot_wait_while() {
    let mutex = Mutex::new(0);
    let cvar = Condvar::new();
    let mut guard = mutex.lock();
    
    // wait_while() takes reference and predicate
    cvar.wait_while(&mut guard, |value| *value < 10);
    
    // Guard now has value >= 10
    println!("Value: {}", *guard);
}

parking_lot::Condvar::wait_while() uses the same reference pattern.

Notification Methods

use std::sync::{Mutex, Condvar};
use std::thread;
use std::sync::Arc;
 
fn std_notify() {
    let mutex = Mutex::new(false);
    let cvar = Condvar::new();
    let arc = Arc::new((mutex, cvar));
    
    let arc_clone = Arc::clone(&arc);
    thread::spawn(move || {
        let (lock, cvar) = &*arc_clone;
        let mut guard = lock.lock().unwrap();
        *guard = true;
        
        // Wake one waiting thread
        cvar.notify_one();
    });
    
    let (lock, cvar) = &*arc;
    let mut guard = lock.lock().unwrap();
    while !*guard {
        guard = cvar.wait(guard).unwrap();
    }
}

Both libraries provide notify_one() and notify_all().

use parking_lot::{Mutex, Condvar};
use std::thread;
use std::sync::Arc;
 
fn parking_lot_notify() {
    let mutex = Mutex::new(false);
    let cvar = Condvar::new();
    let arc = Arc::new((mutex, cvar));
    
    let arc_clone = Arc::clone(&arc);
    thread::spawn(move || {
        let (lock, cvar) = &*arc_clone;
        let mut guard = lock.lock();
        *guard = true;
        
        // Wake one waiting thread
        cvar.notify_one();
    });
    
    let (lock, cvar) = &*arc;
    let mut guard = lock.lock();
    while !*guard {
        cvar.wait(&mut guard);
    }
}

The notification methods are identical in semantics.

Error Handling Differences

use std::sync::{Mutex, Condvar};
use std::thread;
 
fn std_poisoning() {
    let mutex = Mutex::new(0);
    let cvar = Condvar::new();
    
    // If a thread panics while holding the mutex,
    // subsequent lock() returns Err(PoisonError)
    let guard = mutex.lock().unwrap();  // May panic if poisoned
    
    // wait() also returns Result because it reacquires the lock
    let guard = cvar.wait(guard).unwrap();  // May return Err
}

std::sync::Condvar operations return Result for poison handling.

use parking_lot::{Mutex, Condvar};
 
fn parking_lot_no_poison() {
    let mutex = Mutex::new(0);
    let cvar = Condvar::new();
    
    // parking_lot doesn't have poisoning
    // lock() always succeeds
    let mut guard = mutex.lock();
    
    // wait() always succeeds, no Result
    cvar.wait(&mut guard);
}

parking_lot::Condvar doesn't use poison errors.

The Poisoning Trade-off

use std::sync::{Mutex, Condvar};
use std::thread;
 
fn std_poison_example() {
    let mutex = Mutex::new(vec![1, 2, 3]);
    let cvar = Condvar::new();
    let arc = Arc::new((mutex, cvar));
    
    let arc_clone = Arc::clone(&arc);
    let handle = thread::spawn(move || {
        let (lock, cvar) = &*arc_clone;
        let mut data = lock.lock().unwrap();
        data.push(4);
        panic!("Thread panicked!");  // Mutex is now poisoned
    });
    
    handle.join().unwrap_err();  // Catch the panic
    
    let (lock, _) = &*arc;
    match lock.lock() {
        Ok(_) => println!("Mutex recovered"),
        Err(e) => {
            // Can still access data with into_inner()
            let data = e.into_inner();
            println!("Data after panic: {:?}", data);  // [1, 2, 3, 4]
        }
    }
}

Standard library poisoning allows detecting invariant violations.

use parking_lot::{Mutex, Condvar};
use std::thread;
use std::sync::Arc;
 
fn parking_lot_no_poison_example() {
    let mutex = Mutex::new(vec![1, 2, 3]);
    let cvar = Condvar::new();
    let arc = Arc::new((mutex, cvar));
    
    let arc_clone = Arc::clone(&arc);
    let handle = thread::spawn(move || {
        let (lock, cvar) = &*arc_clone;
        let mut data = lock.lock();
        data.push(4);
        panic!("Thread panicked!");
    });
    
    handle.join().unwrap_err();
    
    let (lock, _) = &*arc;
    let data = lock.lock();  // Always succeeds
    println!("Data after panic: {:?}", *data);  // [1, 2, 3, 4]
}

parking_lot continues after panic, which may or may not be desirable.

Spurious Wakeup Handling

use std::sync::{Mutex, Condvar};
 
fn std_spurious_handling() {
    let mutex = Mutex::new(false);
    let cvar = Condvar::new();
    let guard = mutex.lock().unwrap();
    
    // MUST use a loop to handle spurious wakeups
    let mut guard = guard;
    while !*guard {
        guard = cvar.wait(guard).unwrap();
    }
}

Both require loop-based predicate checking for correctness.

use parking_lot::{Mutex, Condvar};
 
fn parking_lot_spurious_handling() {
    let mutex = Mutex::new(false);
    let cvar = Condvar::new();
    let mut guard = mutex.lock();
    
    // Same requirement for predicate checking
    while !*guard {
        cvar.wait(&mut guard);
    }
}

The fundamental requirement for loop-based waiting is identical.

Fairness and Performance

use std::sync::{Mutex, Condvar};
use std::time::Instant;
 
fn std_benchmark() {
    let mutex = Mutex::new(0);
    let cvar = Condvar::new();
    
    let start = Instant::now();
    let guard = mutex.lock().unwrap();
    
    // Standard library has different fairness characteristics
    // May not be as optimized for all cases
    
    println!("Lock acquired in {:?}", start.elapsed());
}

Standard library mutex/condvar prioritizes correctness over raw performance.

use parking_lot::{Mutex, Condvar};
use std::time::Instant;
 
fn parking_lot_benchmark() {
    let mutex = Mutex::new(0);
    let cvar = Condvar::new();
    
    let start = Instant::now();
    let guard = mutex.lock();
    
    // parking_lot is optimized for common cases
    // Generally better performance on contended locks
    
    println!("Lock acquired in {:?}", start.elapsed());
}

parking_lot uses more efficient internal implementations.

Condvar with Different Mutex Types

use std::sync::{Mutex, Condvar};
 
// std::sync::Condvar is designed for std::sync::Mutex
// Can't use with parking_lot::Mutex
 
fn std_type_coupling() {
    let mutex = Mutex::new(0);
    let cvar = Condvar::new();
    
    // This pairing is enforced by the type system
    let guard = mutex.lock().unwrap();
    let _guard = cvar.wait(guard).unwrap();
}

std::sync::Condvar is tightly coupled with std::sync::Mutex.

use parking_lot::{Mutex, Condvar};
 
// parking_lot::Condvar works with parking_lot::Mutex
// Also has consistent API with other parking_lot types
 
fn parking_lot_type_coupling() {
    let mutex = Mutex::new(0);
    let cvar = Condvar::new();
    
    let mut guard = mutex.lock();
    cvar.wait(&mut guard);
}

parking_lot::Condvar is designed to work with parking_lot::Mutex.

Summary Table

Aspect std::sync::Condvar parking_lot::Condvar
Guard parameter Consumes and returns Borrows mutably
Wait return type Result<MutexGuard> Nothing (unit)
Timeout return (Guard, WaitTimeoutResult) bool
Poisoning Returns Result Never poisons
API complexity Higher (Result handling) Lower (direct)
Guard re-binding Required after wait() Not required
Spurious wakeups Possible Possible
Mutex coupling std::sync::Mutex only parking_lot::Mutex only

Synthesis

The ergonomic differences stem from fundamental design philosophies:

Guard handling: std::sync::Condvar requires passing the MutexGuard by value because it must atomically release the lock and wait, then reacquire before returning. This creates the awkward let guard = cvar.wait(guard)? pattern. parking_lot::Condvar achieves the same atomicity through its mutex implementation, allowing it to borrow the guard instead. The mutable reference pattern cvar.wait(&mut guard) is more natural and doesn't require re-binding.

Poisoning philosophy: The standard library treats mutex poisoning as a recoverable error, so every operation returns Result. This adds .unwrap() noise throughout condvar code. parking_lot takes the position that poisoning is rare and usually indicates unrecoverable invariant violation, so it doesn't poison at all. The result is cleaner code but less visibility into panic-induced state corruption.

Timeout simplicity: std::sync::Condvar::wait_timeout() returns a tuple of the guard and a WaitTimeoutResult struct. parking_lot::Condvar::wait_timeout() returns just a bool. The simpler return type covers most use cases: you check if it timed out and continue with the guard you already have.

Key insight: parking_lot::Condvar's ergonomics come from not requiring ownership transfer of the guard. This one design choice cascades into simpler return types, no Result wrapping, and no re-binding after wait. The trade-off is losing poisoning detection, which is valuable for some systems and unnecessary for others. Choose std::sync::Condvar when you need poisoning semantics for invariant detection; choose parking_lot::Condvar for cleaner code in systems where panics are treated as unrecoverable anyway.