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.
