How does parking_lot::Condvar compare to std::sync::Condvar for condition variable synchronization?

Condition variables enable threads to wait for a condition to become true, coordinating access to shared state protected by a mutex. The parking_lot::Condvar improves upon std::sync::Condvar in several ways: it doesn't require handling PoisonError since parking_lot mutexes don't poison, it supports timeout-based waits with better precision, it provides notify_all behavior by default (matching common usage patterns), and it integrates seamlessly with parking_lot::Mutex. The API is also cleaner—wait methods return bool indicating whether the condition was signaled (versus spurious wakeup), and timeout methods return WaitTimeoutResult with precise timing information. This makes parking_lot::Condvar more ergonomic for implementing producer-consumer patterns, thread pools, and other synchronization primitives.

Basic Condition Variable Usage

use std::sync::{Condvar as StdCondvar, Mutex as StdMutex};
use parking_lot::{Condvar, Mutex};
 
fn basic_comparison() {
    // std::sync::Condvar with std::sync::Mutex
    let std_pair = (StdMutex::new(false), StdCondvar::new());
    {
        let (lock, cvar) = &std_pair;
        let mut guard = lock.lock().unwrap();
        while !*guard {
            guard = cvar.wait(guard).unwrap();
        }
    }
    
    // parking_lot::Condvar with parking_lot::Mutex
    let parking_pair = (Mutex::new(false), Condvar::new());
    {
        let (lock, cvar) = &parking_pair;
        let mut guard = lock.lock();
        while !*guard {
            cvar.wait(&mut guard);
        }
    }
}

The parking_lot version avoids unwrap() calls since there's no poisoning.

The Poisoning Difference

use std::sync::{Condvar as StdCondvar, Mutex as StdMutex};
use parking_lot::{Condvar, Mutex};
use std::sync::Arc;
use std::thread;
 
fn poisoning_comparison() {
    // std::sync::Condvar requires handling poison
    let std_pair = Arc::new((StdMutex::new(0), StdCondvar::new()));
    let std_clone = Arc::clone(&std_pair);
    
    let handle = thread::spawn(move || {
        let (lock, _) = &*std_clone;
        let _guard = lock.lock().unwrap();
        panic!("Thread panics while holding lock");
    });
    
    handle.join().unwrap_err();
    
    // Now the mutex is poisoned
    {
        let (lock, cvar) = &*std_pair;
        let result = lock.lock();
        match result {
            Ok(guard) => {
                // Normal case
                let _ = cvar.wait(guard);
            }
            Err(poison_error) => {
                // Must handle poisoned state
                println!("Mutex was poisoned: {:?}", poison_error);
            }
        }
    }
    
    // parking_lot::Condvar has no poison concern
    let parking_pair = Arc::new((Mutex::new(0), Condvar::new()));
    let parking_clone = Arc::clone(&parking_pair);
    
    let handle = thread::spawn(move || {
        let (lock, _) = &*parking_clone;
        let _guard = lock.lock();
        panic!("Thread panics while holding lock");
    });
    
    handle.join().unwrap_err();
    
    // Condvar and Mutex still work normally
    {
        let (lock, cvar) = &*parking_pair;
        let mut guard = lock.lock(); // No Result, no poisoning
        *guard = 42;
        cvar.notify_one(); // Works fine
    }
}

parking_lot eliminates poison handling, simplifying error paths.

Wait Method Signatures

use std::sync::{Condvar as StdCondvar, Mutex as StdMutex};
use parking_lot::{Condvar, Mutex};
 
fn wait_signatures() {
    // std::sync::Condvar::wait
    // Returns Result<MutexGuard, PoisonError>
    // Must handle both cases
    let std_lock = StdMutex::new(0);
    let std_cvar = StdCondvar::new();
    {
        let guard = std_lock.lock().unwrap();
        // Returns Result - guard is moved, must handle Result
        let guard = std_cvar.wait(guard).unwrap();
    }
    
    // parking_lot::Condvar::wait
    // Returns () - no Result, no poisoning
    let parking_lock = Mutex::new(0);
    let parking_cvar = Condvar::new();
    {
        let mut guard = parking_lock.lock();
        // Takes &mut MutexGuard, no return value needed
        parking_cvar.wait(&mut guard);
        // Guard is still valid
    }
}

The parking_lot API borrows the guard mutably instead of moving it.

Wait with Predicate

use parking_lot::{Condvar, Mutex};
use std::sync::Arc;
use std::thread;
 
fn wait_while_loop() {
    let pair = Arc::new((Mutex::new(false), Condvar::new()));
    let pair_clone = Arc::clone(&pair);
    
    // Spawner thread
    let handle = thread::spawn(move || {
        thread::sleep(std::time::Duration::from_millis(100));
        let (lock, cvar) = &*pair_clone;
        let mut guard = lock.lock();
        *guard = true;
        cvar.notify_one();
    });
    
    // Waiting thread
    {
        let (lock, cvar) = &*pair;
        let mut guard = lock.lock();
        
        // Must loop to handle spurious wakeups
        while !*guard {
            cvar.wait(&mut guard);
        }
        
        println!("Condition met: {}", *guard);
    }
    
    handle.join().unwrap();
}
 
fn wait_until_method() {
    // parking_lot provides wait_until for convenience
    let pair = Arc::new((Mutex::new(Vec::new()), Condvar::new()));
    let pair_clone = Arc::clone(&pair);
    
    // Producer
    let handle = thread::spawn(move || {
        let (lock, cvar) = &*pair_clone;
        let mut guard = lock.lock();
        guard.push(42);
        cvar.notify_one();
    });
    
    // Consumer waits until predicate is true
    {
        let (lock, cvar) = &*pair;
        let mut guard = lock.lock();
        
        // wait_until loops internally
        cvar.wait_until(&mut guard, |g| !g.is_empty());
        
        println!("Got item: {}", guard[0]);
    }
    
    handle.join().unwrap();
}

wait_until encapsulates the common loop pattern.

wait_until Return Value

use parking_lot::{Condvar, Mutex};
 
fn wait_until_return() {
    let pair = (Mutex::new(0), Condvar::new());
    let (lock, cvar) = &pair;
    let mut guard = lock.lock();
    
    // wait_until returns bool indicating if condition was signaled
    // vs spurious wakeup
    let result = cvar.wait_until(&mut guard, |g| *g > 0);
    
    // In this case, would wait forever since nobody signals
    // But if signaled, returns true
    match result {
        true => println!("Condition satisfied"),
        false => println!("Spurious wakeup (shouldn't happen with wait_until)"),
    }
}

wait_until returns bool indicating the result.

Timeout-Based Waiting

use parking_lot::{Condvar, Mutex};
use std::time::{Duration, Instant};
 
fn timeout_wait() {
    let pair = (Mutex::new(false), Condvar::new());
    let (lock, cvar) = &pair;
    
    // wait_for with Duration
    {
        let mut guard = lock.lock();
        
        // Returns WaitTimeoutResult
        let result = cvar.wait_for(&mut guard, Duration::from_millis(100));
        
        if result.timed_out() {
            println!("Wait timed out after 100ms");
        } else {
            println!("Condition signaled before timeout");
        }
    }
    
    // wait_until with Instant
    {
        let mut guard = lock.lock();
        
        let deadline = Instant::now() + Duration::from_millis(50);
        let result = cvar.wait_until_timeout(&mut guard, deadline);
        
        if result.timed_out() {
            println!("Timed out at deadline");
        }
    }
}
 
fn std_timeout_comparison() {
    // std::sync::Condvar timeout
    use std::sync::{Condvar as StdCondvar, Mutex as StdMutex};
    
    let pair = (StdMutex::new(false), StdCondvar::new());
    let (lock, cvar) = &pair;
    let guard = lock.lock().unwrap();
    
    let result = cvar.wait_timeout(guard, Duration::from_millis(100));
    // Returns (MutexGuard, WaitTimeoutResult)
    // Must handle unwrap again
    
    // parking_lot returns cleaner result
    let pair = (Mutex::new(false), Condvar::new());
    let (lock, cvar) = &pair;
    let mut guard = lock.lock();
    
    let result = cvar.wait_for(&mut guard, Duration::from_millis(100));
    // Returns WaitTimeoutResult directly
}

Timeout handling is cleaner with parking_lot due to consistent return types.

Producer-Consumer Pattern

use parking_lot::{Condvar, Mutex};
use std::collections::VecDeque;
use std::sync::Arc;
use std::thread;
 
struct Queue<T> {
    data: Mutex<VecDeque<T>>,
    not_empty: Condvar,
    not_full: Condvar,
    capacity: usize,
}
 
impl<T> Queue<T> {
    fn new(capacity: usize) -> Self {
        Queue {
            data: Mutex::new(VecDeque::with_capacity(capacity)),
            not_empty: Condvar::new(),
            not_full: Condvar::new(),
            capacity,
        }
    }
    
    fn push(&self, item: T) {
        let mut data = self.data.lock();
        
        // Wait until there's space
        while data.len() >= self.capacity {
            self.not_full.wait(&mut data);
        }
        
        data.push_back(item);
        self.not_empty.notify_one();
    }
    
    fn pop(&self) -> T {
        let mut data = self.data.lock();
        
        // Wait until there's data
        while data.is_empty() {
            self.not_empty.wait(&mut data);
        }
        
        let item = data.pop_front().unwrap();
        self.not_full.notify_one();
        item
    }
    
    fn try_pop(&self, timeout: std::time::Duration) -> Option<T> {
        let mut data = self.data.lock();
        
        // Wait with timeout until data available
        let result = self.not_empty.wait_for(&mut data, timeout);
        if result.timed_out() || data.is_empty() {
            return None;
        }
        
        let item = data.pop_front();
        self.not_full.notify_one();
        item
    }
}
 
fn producer_consumer() {
    let queue = Arc::new(Queue::<i32>::new(10));
    
    // Producer
    let producer_queue = Arc::clone(&queue);
    let producer = thread::spawn(move || {
        for i in 0..100 {
            producer_queue.push(i);
        }
    });
    
    // Consumer
    let consumer_queue = Arc::clone(&queue);
    let consumer = thread::spawn(move || {
        for _ in 0..100 {
            let item = consumer_queue.pop();
            println!("Consumed: {}", item);
        }
    });
    
    producer.join().unwrap();
    consumer.join().unwrap();
}

Producer-consumer queues benefit from clean condition variable API.

Notify Methods

use parking_lot::{Condvar, Mutex};
 
fn notify_methods() {
    let pair = (Mutex::new(0), Condvar::new());
    let (lock, cvar) = &pair;
    
    // notify_one wakes one waiting thread
    cvar.notify_one();
    
    // notify_all wakes all waiting threads
    cvar.notify_all();
}
 
fn std_notify_comparison() {
    use std::sync::{Condvar as StdCondvar, Mutex as StdMutex};
    
    let pair = (StdMutex::new(0), StdCondvar::new());
    let (lock, cvar) = &pair;
    
    // Same methods, but must handle poisoning
    cvar.notify_one();
    cvar.notify_all();
}

Both provide notify_one and notify_all, but semantics differ slightly.

Multiple Waiters Scenario

use parking_lot::{Condvar, Mutex};
use std::sync::Arc;
use std::thread;
 
fn multiple_waiters() {
    let pair = Arc::new((Mutex::new(0), Condvar::new()));
    let mut handles = Vec::new();
    
    // Spawn multiple waiting threads
    for i in 0..5 {
        let pair_clone = Arc::clone(&pair);
        let handle = thread::spawn(move || {
            let (lock, cvar) = &*pair_clone;
            let mut guard = lock.lock();
            
            cvar.wait_until(&mut guard, |g| *g > 0);
            println!("Thread {} woke up", i);
        });
        handles.push(handle);
    }
    
    // Give threads time to start waiting
    thread::sleep(std::time::Duration::from_millis(50));
    
    // Wake one thread
    {
        let (lock, cvar) = &*pair;
        let mut guard = lock.lock();
        *guard = 1;
        cvar.notify_one();
    }
    
    // Only one thread wakes
    // Need notify_all to wake all
    
    thread::sleep(std::time::Duration::from_millis(50));
    
    // Wake all remaining threads
    {
        let (lock, cvar) = &*pair;
        let mut guard = lock.lock();
        *guard = 2;
        cvar.notify_all();
    }
    
    for handle in handles {
        handle.join().unwrap();
    }
}

notify_one wakes one thread; notify_all wakes all waiting threads.

Thread Pool Shutdown Pattern

use parking_lot::{Condvar, Mutex};
use std::sync::Arc;
use std::thread;
 
struct ThreadPool {
    workers: Mutex<Vec<Option<thread::JoinHandle<()>>>>,
    shutdown: Mutex<bool>,
    shutdown_cvar: Condvar,
}
 
impl ThreadPool {
    fn new(num_workers: usize) -> Arc<Self> {
        let pool = Arc::new(ThreadPool {
            workers: Mutex::new(Vec::with_capacity(num_workers)),
            shutdown: Mutex::new(false),
            shutdown_cvar: Condvar::new(),
        });
        
        for _ in 0..num_workers {
            let pool_clone = Arc::clone(&pool);
            let worker = thread::spawn(move || {
                loop {
                    // Check for shutdown
                    {
                        let shutdown = pool_clone.shutdown.lock();
                        if *shutdown {
                            break;
                        }
                    }
                    
                    // Do work...
                    // For demo, just sleep
                    thread::sleep(std::time::Duration::from_millis(10));
                }
            });
            
            pool.workers.lock().push(Some(worker));
        }
        
        pool
    }
    
    fn shutdown(&self) {
        // Signal shutdown
        {
            let mut shutdown = self.shutdown.lock();
            *shutdown = true;
        }
        
        // Wake all workers
        self.shutdown_cvar.notify_all();
        
        // Wait for workers
        let mut workers = self.workers.lock();
        for worker in workers.iter_mut() {
            if let Some(handle) = worker.take() {
                handle.join().unwrap();
            }
        }
    }
}

Condition variables coordinate shutdown across multiple threads.

Spurious Wakeup Handling

use parking_lot::{Condvar, Mutex};
 
fn spurious_wakeup_handling() {
    let pair = (Mutex::new(false), Condvar::new());
    let (lock, cvar) = &pair;
    let mut guard = lock.lock();
    
    // Incorrect: might wake without condition being true
    // cvar.wait(&mut guard);
    // if *guard { ... } // May be false!
    
    // Correct: always check condition in loop
    while !*guard {
        cvar.wait(&mut guard);
    }
    
    // Correct: use wait_until which handles the loop
    cvar.wait_until(&mut guard, |g| *g);
}

Always check the condition in a loop or use wait_until to handle spurious wakeups.

Comparison Table

fn comparison_summary() {
    // std::sync::Condvar
    // - Returns Result from wait (poison handling)
    // - MutexGuard moved, returned from wait
    // - wait_timeout returns (guard, result) tuple
    // - No wait_until convenience method
    // - Works only with std::sync::Mutex
    
    // parking_lot::Condvar
    // - No Result from wait (no poisoning)
    // - MutexGuard borrowed mutably
    // - wait_for returns WaitTimeoutResult directly
    // - wait_until convenience method
    // - Works only with parking_lot::Mutex
}

The API differences reflect the underlying design philosophy.

When to Use Each

fn when_to_use() {
    // Use std::sync::Condvar when:
    // 1. Using std::sync::Mutex (must match)
    // 2. Want poisoning semantics
    // 3. Prefer standard library only
    // 4. Need to handle poison explicitly
    
    // Use parking_lot::Condvar when:
    // 1. Using parking_lot::Mutex (must match)
    // 2. Want cleaner API without Result unwrapping
    // 3. Need wait_until convenience
    // 4. Want timeout methods that don't return guards
    // 5. Consistent behavior across platforms
    // 6. No poisoning concern preferred
}

Match condition variable type with mutex type—std::sync::Condvar with std::sync::Mutex, parking_lot::Condvar with parking_lot::Mutex.

Real-World Example: Rate Limiter

use parking_lot::{Condvar, Mutex};
use std::sync::Arc;
use std::time::{Duration, Instant};
 
struct RateLimiter {
    state: Mutex<RateLimiterState>,
    cvar: Condvar,
}
 
struct RateLimiterState {
    tokens: u32,
    max_tokens: u32,
    refill_rate: Duration,
    last_refill: Instant,
}
 
impl RateLimiter {
    fn new(max_tokens: u32, refill_rate: Duration) -> Self {
        RateLimiter {
            state: Mutex::new(RateLimiterState {
                tokens: max_tokens,
                max_tokens,
                refill_rate,
                last_refill: Instant::now(),
            }),
            cvar: Condvar::new(),
        }
    }
    
    fn acquire(&self) {
        let mut state = self.state.lock();
        
        // Wait until a token is available
        self.cvar.wait_until(&mut state, |s| {
            // Refill tokens based on time elapsed
            let now = Instant::now();
            let elapsed = now.duration_since(s.last_refill);
            let tokens_to_add = (elapsed.as_nanos() / s.refill_rate.as_nanos()) as u32;
            
            if tokens_to_add > 0 {
                s.tokens = (s.tokens + tokens_to_add).min(s.max_tokens);
                s.last_refill = now;
            }
            
            s.tokens > 0
        });
        
        state.tokens -= 1;
    }
    
    fn try_acquire(&self, timeout: Duration) -> bool {
        let mut state = self.state.lock();
        
        let result = self.cvar.wait_for(&mut state, timeout);
        if result.timed_out() {
            return false;
        }
        
        if state.tokens > 0 {
            state.tokens -= 1;
            true
        } else {
            false
        }
    }
}
 
fn rate_limiter_usage() {
    let limiter = Arc::new(RateLimiter::new(5, Duration::from_millis(100)));
    
    let mut handles = Vec::new();
    for _ in 0..20 {
        let limiter = Arc::clone(&limiter);
        handles.push(std::thread::spawn(move || {
            limiter.acquire();
            println!("Acquired token at {:?}", Instant::now());
        }));
    }
    
    for handle in handles {
        handle.join().unwrap();
    }
}

Rate limiters use condition variables to wait until tokens are available.

Synthesis

Key differences:

Feature std::sync::Condvar parking_lot::Condvar
Poisoning Returns Result No poisoning, direct return
Wait signature wait(guard) -> Result<Guard, PoisonError> wait(&mut guard)
Timeout method wait_timeout(guard, dur) -> Result<(Guard, WaitTimeoutResult), PoisonError> wait_for(&mut guard, dur) -> WaitTimeoutResult
Convenience No wait_until wait_until(&mut guard, predicate)
Mutex pairing std::sync::Mutex parking_lot::Mutex

Method comparison:

Operation std parking_lot
Basic wait wait(guard).unwrap() wait(&mut guard)
Timed wait wait_timeout(guard, dur).unwrap() wait_for(&mut guard, dur)
With predicate Manual loop wait_until(&mut guard, |g| ...)
Notify one notify_one() notify_one()
Notify all notify_all() notify_all()

Common patterns:

Pattern Implementation
Wait for condition while !cond { cvar.wait(&mut guard); }
Wait with predicate cvar.wait_until(&mut guard, |g| cond);
Timeout wait let r = cvar.wait_for(&mut guard, dur); if r.timed_out() { ... }
Shutdown signal *shutdown = true; cvar.notify_all();

Key insight: parking_lot::Condvar provides a cleaner, more ergonomic API than std::sync::Condvar by eliminating poisoning concerns and offering wait_until for the common predicate-wait pattern. The signature difference—borrowing &mut MutexGuard instead of consuming and returning the guard—makes code more readable and eliminates the noise of unwrap() calls. However, the types are not interchangeable: parking_lot::Condvar must be used with parking_lot::Mutex, and the same applies to the standard library versions. The timeout methods in parking_lot return WaitTimeoutResult directly, making it clear whether the wait timed out without needing to destructure tuples. For production code where panic recovery isn't needed and you're already using parking_lot::Mutex, the Condvar is the natural choice. Use std::sync::Condvar when you need poisoning semantics or are committed to standard library types only.