How does parking_lot::Condvar::wait_until differ from wait for predicate-based condition waiting?
wait_until encapsulates the predicate-checking loop that guards against spurious wakeups, replacing the manual while !predicate { condvar.wait() } pattern with a single call that atomically checks the condition and blocks only when necessary, while wait requires explicit loop-based predicate checking to be correct. This distinction matters because condition variables can wake spuriously, and wait_until ensures correct predicate evaluation without requiring developers to remember the loop pattern.
The Condition Variable Problem
use parking_lot::{Mutex, Condvar};
use std::sync::Arc;
use std::thread;
use std::time::Duration;
fn condition_variable_problem() {
// Condition variables enable threads to wait for a state change
// The classic pattern: wait for some condition to become true
let pair = Arc::new((Mutex::new(false), Condvar::new()));
let pair_clone = Arc::clone(&pair);
// Spawner thread: sets the condition
thread::spawn(move || {
thread::sleep(Duration::from_millis(100));
let (lock, cvar) = &*pair_clone;
*lock.lock() = true;
cvar.notify_one();
});
// Waiter thread: waits for condition
let (lock, cvar) = &*pair;
let mut started = lock.lock();
// Problem: How do we wait correctly?
// - cvar.wait() might return spuriously (without notify)
// - Must re-check the condition after waking
// - Must hold lock while checking
}Condition variables require careful predicate handling; incorrect patterns lead to bugs.
Spurious Wakeups: The Hidden Danger
use parking_lot::{Mutex, Condvar};
use std::sync::Arc;
use std::thread;
fn spurious_wakeup_danger() {
let pair = Arc::new((Mutex::new(0u32), Condvar::new()));
let (lock, cvar) = &*pair;
// Spurious wakeups: condvar.wait() can return WITHOUT notify
// POSIX allows this for implementation efficiency
// Your code MUST handle it correctly
let mut counter = lock.lock();
// WRONG: Using if instead of while
// This will occasionally fail due to spurious wakeups
if *counter < 10 {
cvar.wait(&mut counter);
// If we wake spuriously, *counter might still be < 10
// But we proceed as if it's >= 10
}
// This is a BUG - spurious wakeups cause incorrect behavior
// CORRECT: Using while loop
while *counter < 10 {
cvar.wait(&mut counter);
// Re-check condition after every wakeup
// Spurious wakeup? Loop continues, waits again
// Real wakeup? Condition might be true, exit loop
}
// Now *counter >= 10 is guaranteed
}The while loop pattern is essential for correctness because spurious wakeups are permitted.
The Basic wait Method
use parking_lot::{Mutex, Condvar};
use std::sync::Arc;
use std::thread;
use std::time::Duration;
fn basic_wait_pattern() {
let data = Arc::new((Mutex::new(Vec::new()), Condvar::new()));
// Producer thread
let producer_data = Arc::clone(&data);
thread::spawn(move || {
let (lock, cvar) = &*producer_data;
let mut queue = lock.lock();
for i in 0..5 {
queue.push(i);
cvar.notify_one(); // Signal that data is available
thread::sleep(Duration::from_millis(50));
}
});
// Consumer thread using wait()
let (lock, cvar) = &*data;
let mut queue = lock.lock();
// Manual loop pattern required with wait()
while queue.is_empty() {
// wait() releases the lock and blocks
// When signaled, re-acquires lock and returns
// We MUST loop to re-check the condition
cvar.wait(&mut queue);
}
// Process items
while let Some(item) = queue.pop() {
println!("Got: {}", item);
}
}wait requires the caller to write the predicate loop; forgetting it causes subtle bugs.
The wait_until Method
use parking_lot::{Mutex, Condvar};
use std::sync::Arc;
use std::thread;
use std::time::Duration;
fn wait_until_pattern() {
let data = Arc::new((Mutex::new(Vec::new()), Condvar::new()));
// Producer thread
let producer_data = Arc::clone(&data);
thread::spawn(move || {
let (lock, cvar) = &*producer_data;
let mut queue = lock.lock();
for i in 0..5 {
queue.push(i);
cvar.notify_one();
thread::sleep(Duration::from_millis(50));
}
});
// Consumer thread using wait_until()
let (lock, cvar) = &*data;
let mut queue = lock.lock();
// wait_until handles the loop for us
// Pass a predicate function that returns true when condition is met
cvar.wait_until(&mut queue, |queue| {
!queue.is_empty() // Wait until queue is not empty
});
// When wait_until returns, predicate is guaranteed true
// Queue is definitely not empty
while let Some(item) = queue.pop() {
println!("Got: {}", item);
}
}wait_until encapsulates the predicate loop, guaranteeing the condition holds when it returns.
Predicate-Based Waiting Explained
use parking_lot::{Mutex, Condvar};
use std::sync::Arc;
fn predicate_waiting() {
let pair = Arc::new((Mutex::new(0u32), Condvar::new()));
let (lock, cvar) = &*pair;
let mut value = lock.lock();
// wait_until takes a closure (predicate)
// The closure receives the locked value
// Pattern: wait_until(&mut guard, |data| condition)
cvar.wait_until(&mut value, |v| *v >= 10);
// This is equivalent to:
while *value < 10 {
cvar.wait(&mut value);
}
// But wait_until ensures:
// 1. The predicate is checked before waiting
// 2. The predicate is re-checked after every wakeup
// 3. The function returns only when predicate is true
}The predicate closure receives the guarded value and returns true when waiting should stop.
Implementation Comparison
use parking_lot::{Mutex, Condvar};
use std::sync::Arc;
use std::thread;
fn implementation_comparison() {
let pair = Arc::new((Mutex::new(0u32), Condvar::new()));
// Method 1: Manual loop with wait()
let pair1 = Arc::clone(&pair);
thread::spawn(move || {
let (lock, cvar) = &*pair1;
let mut value = lock.lock();
// Manual loop - you must remember the pattern
while *value < 10 {
cvar.wait(&mut value);
}
// *value >= 10 is true
println!("Method 1: value = {}", *value);
});
// Method 2: wait_until with predicate
let pair2 = Arc::clone(&pair);
thread::spawn(move || {
let (lock, cvar) = &*pair2;
let mut value = lock.lock();
// wait_until handles the loop
cvar.wait_until(&mut value, |v| *v >= 10);
// *value >= 10 is guaranteed
println!("Method 2: value = {}", *value);
});
// Both produce identical behavior
// wait_until is less error-prone
}Both approaches produce the same result, but wait_until is safer and more expressive.
Complex Predicates
use parking_lot::{Mutex, Condvar};
use std::sync::Arc;
use std::thread;
use std::collections::VecDeque;
fn complex_predicates() {
struct SharedState {
queue: VecDeque<String>,
shutdown: bool,
}
let state = Arc::new((Mutex::new(SharedState {
queue: VecDeque::new(),
shutdown: false,
}), Condvar::new()));
// Wait for queue to have items OR shutdown
let (lock, cvar) = &*state;
let mut guard = lock.lock();
cvar.wait_until(&mut guard, |state| {
!state.queue.is_empty() || state.shutdown
});
// Now: either queue has items, or we're shutting down
if guard.shutdown {
println!("Shutting down");
} else {
println!("Got item: {:?}", guard.queue.pop_front());
}
// Wait for specific condition
cvar.wait_until(&mut guard, |state| {
state.queue.len() >= 5 || state.shutdown
});
// Wait for item matching criteria
cvar.wait_until(&mut guard, |state| {
state.queue.front().map(|s| s.starts_with("priority:")).unwrap_or(false)
});
}Complex predicates can check multiple conditions; wait_until handles them uniformly.
Timeout Support
use parking_lot::{Mutex, Condvar};
use std::sync::Arc;
use std::time::{Duration, Instant};
fn timeout_support() {
let pair = Arc::new((Mutex::new(0u32), Condvar::new()));
let (lock, cvar) = &*pair;
let mut value = lock.lock();
// wait_until with timeout
let deadline = Instant::now() + Duration::from_secs(5);
let result = cvar.wait_until(&mut value, deadline, |v| *v >= 10);
// wait_until with timeout returns a WaitUntilResult
// It has a .is_success() method
if result.is_success() {
println!("Condition met: value = {}", *value);
} else {
println!("Timeout elapsed, value = {}", *value);
}
// Equivalent manual pattern with wait_timeout:
let deadline = Instant::now() + Duration::from_secs(5);
while *value < 10 {
let timeout_result = cvar.wait_until(&mut value, deadline);
if timeout_result.timed_out() {
break;
}
}
// The wait_until version is cleaner and harder to get wrong
}wait_until with deadlines combines predicate checking with timeout handling.
Return Value of wait_until
use parking_lot::{Mutex, Condvar};
use std::sync::Arc;
use std::time::{Duration, Instant};
fn wait_until_return_value() {
let pair = Arc::new((Mutex::new(0u32), Condvar::new()));
let (lock, cvar) = &*pair;
let mut value = lock.lock();
// Without timeout: returns MutexGuard
// The guard is returned when predicate becomes true
cvar.wait_until(&mut value, |v| *v >= 10);
// value is still locked and *value >= 10
// With timeout: returns WaitUntilResult
let deadline = Instant::now() + Duration::from_secs(5);
let result = cvar.wait_until(&mut value, deadline, |v| *v >= 10);
// Check if condition was met or timed out
if result.is_success() {
// Condition met within timeout
// *value >= 10 is guaranteed
} else {
// Timed out
// *value might be anything (< 10, or >= 10 if condition became true)
// Check the actual value
}
// The MutexGuard is still held in both cases
// value remains locked
}wait_until returns the guard (or result with guard access), maintaining lock ownership semantics.
Common Mistakes with wait()
use parking_lot::{Mutex, Condvar};
use std::sync::Arc;
fn common_mistakes() {
let pair = Arc::new((Mutex::new(0u32), Condvar::new()));
let (lock, cvar) = &*pair;
// MISTAKE 1: Using if instead of while
{
let mut value = lock.lock();
if *value < 10 { // WRONG: should be while
cvar.wait(&mut value);
// Spurious wakeup can reach here even if value < 10
}
// *value >= 10 is NOT guaranteed
}
// MISTAKE 2: Checking condition before lock
{
// WRONG: Checking without lock
// if *value < 10 { ... } // value isn't locked!
let mut value = lock.lock();
while *value < 10 {
cvar.wait(&mut value);
}
}
// MISTAKE 3: Modifying condition without notify
{
let mut value = lock.lock();
*value = 20; // Changed condition
// Forgot cvar.notify_one() or notify_all()!
// Waiting threads may never wake
}
// wait_until prevents mistake 1 by encapsulating the loop
{
let mut value = lock.lock();
cvar.wait_until(&mut value, |v| *v >= 10);
// Guaranteed: *value >= 10
// No spurious wakeup bug possible
}
}wait_until eliminates the most common mistake: forgetting the while loop.
Performance Considerations
use parking_lot::{Mutex, Condvar};
use std::sync::Arc;
fn performance_notes() {
// wait_until is NOT slower than manual loop
// Both compile to essentially the same code
let pair = Arc::new((Mutex::new(0u32), Condvar::new()));
let (lock, cvar) = &*pair;
let mut value = lock.lock();
// Manual loop (what wait_until does internally):
while *value < 10 {
cvar.wait(&mut value);
}
// wait_until equivalent:
cvar.wait_until(&mut value, |v| *v >= 10);
// Performance characteristics:
// - Same number of wakeups and lock acquisitions
// - Same spurious wakeup handling
// - Minimal overhead from closure call (often inlined)
// The closure is called:
// 1. Before waiting (to check if already true)
// 2. After each wakeup (to check if condition met)
// Same as manual loop, just cleaner syntax
// For very hot paths, the closure overhead exists but is tiny
// Usually negligible compared to lock contention
}wait_until has negligible overhead compared to manual loops; clarity wins.
Producer-Consumer Pattern
use parking_lot::{Mutex, Condvar};
use std::sync::Arc;
use std::thread;
use std::collections::VecDeque;
fn producer_consumer() {
struct Queue {
items: VecDeque<u32>,
closed: bool,
}
let state = Arc::new((Mutex::new(Queue { items: VecDeque::new(), closed: false }), Condvar::new()));
// Producer
let producer_state = Arc::clone(&state);
let producer = thread::spawn(move || {
let (lock, cvar) = &*producer_state;
let mut queue = lock.lock();
for i in 0..10 {
queue.items.push_back(i);
cvar.notify_one(); // Wake one waiting consumer
}
queue.closed = true;
cvar.notify_all(); // Wake all consumers to check closed flag
});
// Consumer using wait_until
let consumer_state = Arc::clone(&state);
let consumer = thread::spawn(move || {
let (lock, cvar) = &*consumer_state;
let mut queue = lock.lock();
loop {
// Wait for items or closed
cvar.wait_until(&mut queue, |q| !q.items.is_empty() || q.closed);
// Process all available items
while let Some(item) = queue.items.pop_front() {
println!("Consumed: {}", item);
}
if queue.closed {
break;
}
}
});
producer.join().unwrap();
consumer.join().unwrap();
}The producer-consumer pattern benefits from wait_until's clear predicate expression.
Thread Pool Shutdown Pattern
use parking_lot::{Mutex, Condvar};
use std::sync::Arc;
use std::thread;
fn thread_pool_shutdown() {
struct PoolState {
active_jobs: usize,
shutdown: bool,
}
let state = Arc::new((Mutex::new(PoolState { active_jobs: 0, shutdown: false }), Condvar::new()));
// Worker threads
let workers: Vec<_> = (0..4)
.map(|id| {
let worker_state = Arc::clone(&state);
thread::spawn(move || {
let (lock, cvar) = &*worker_state;
let mut state = lock.lock();
loop {
// Wait for work or shutdown
cvar.wait_until(&mut state, |s| s.shutdown || s.active_jobs > 0);
if state.shutdown && state.active_jobs == 0 {
break; // Exit thread
}
if state.active_jobs > 0 {
state.active_jobs -= 1;
drop(state); // Release lock while working
// Do work...
println!("Worker {} processing", id);
state = lock.lock(); // Re-acquire
}
}
})
})
.collect();
// Shutdown: wait for all jobs to complete
let (lock, cvar) = &*state;
let mut state = lock.lock();
state.shutdown = true;
cvar.notify_all(); // Wake all workers
// Wait for all jobs to finish
cvar.wait_until(&mut state, |s| s.active_jobs == 0);
for worker in workers {
worker.join().unwrap();
}
}Shutdown patterns use wait_until to wait for complex state conditions safely.
Bounded Buffer Pattern
use parking_lot::{Mutex, Condvar};
use std::sync::Arc;
use std::collections::VecDeque;
fn bounded_buffer() {
struct Buffer<T> {
items: VecDeque<T>,
capacity: usize,
}
let buffer = Arc::new((
Mutex::new(Buffer { items: VecDeque::new(), capacity: 5 }),
Condvar::new(),
Condvar::new(), // Two condvars: not_empty and not_full
));
let (lock, not_empty, not_full) = &*buffer;
// Producer: wait until not full
{
let mut buf = lock.lock();
not_full.wait_until(&mut buf, |b| b.items.len() < b.capacity);
buf.items.push_back(42);
not_empty.notify_one(); // Signal consumers
}
// Consumer: wait until not empty
{
let mut buf = lock.lock();
not_empty.wait_until(&mut buf, |b| !b.items.is_empty());
let item = buf.items.pop_front();
not_full.notify_one(); // Signal producers
}
}Bounded buffers use wait_until to wait for space or data availability.
Comparing std::sync vs parking_lot
use parking_lot::{Mutex, Condvar};
use std::sync::{Mutex as StdMutex, Condvar as StdCondvar};
use std::sync::Arc;
fn comparing_implementations() {
// std::sync version (requires MutexGuard)
let std_pair = Arc::new((StdMutex::new(0u32), StdCondvar::new()));
{
let (lock, cvar) = &*std_pair;
let mut value = lock.lock().unwrap();
// std::sync::Condvar::wait takes MutexGuard
while *value < 10 {
value = cvar.wait(value).unwrap();
}
// std::sync::Condvar::wait_while (equivalent to parking_lot's wait_until)
value = cvar.wait_while(value, |v| *v < 10).unwrap();
}
// parking_lot version (requires MutexGuard)
let pl_pair = Arc::new((Mutex::new(0u32), Condvar::new()));
{
let (lock, cvar) = &*pl_pair;
let mut value = lock.lock();
// parking_lot::Condvar::wait
while *value < 10 {
cvar.wait(&mut value);
}
// parking_lot::Condvar::wait_until
cvar.wait_until(&mut value, |v| *v >= 10);
}
// Key differences:
// 1. parking_lot doesn't need unwrap() (no poisoning)
// 2. parking_lot takes &mut MutexGuard, std takes ownership
// 3. parking_lot's wait_until is cleaner than std's wait_while
// 4. parking_lot generally has better performance
}parking_lot provides cleaner API and better performance than std::sync.
Complete Example: Job Queue
use parking_lot::{Mutex, Condvar};
use std::sync::Arc;
use std::thread;
use std::collections::VecDeque;
use std::time::Duration;
fn complete_job_queue_example() {
struct JobQueue {
jobs: VecDeque<String>,
shutdown: bool,
active_workers: usize,
}
let state = Arc::new((
Mutex::new(JobQueue {
jobs: VecDeque::new(),
shutdown: false,
active_workers: 0,
}),
Condvar::new(),
));
// Worker function
fn worker(id: usize, state: Arc<(Mutex<JobQueue>, Condvar)>) {
let (lock, cvar) = &*state;
let mut queue = lock.lock();
loop {
// Wait for: jobs available OR shutdown with no active work
cvar.wait_until(&mut queue, |q| {
!q.jobs.is_empty() || (q.shutdown && q.active_workers == 0)
});
// Check shutdown condition
if queue.shutdown && queue.jobs.is_empty() {
break;
}
// Get job
if let Some(job) = queue.jobs.pop_front() {
queue.active_workers += 1;
drop(queue); // Release lock while working
println!("Worker {} processing: {}", id, job);
thread::sleep(Duration::from_millis(100)); // Simulate work
queue = lock.lock();
queue.active_workers -= 1;
cvar.notify_all(); // Notify that active_workers changed
}
}
}
// Spawn workers
let workers: Vec<_> = (0..4)
.map(|id| {
let state = Arc::clone(&state);
thread::spawn(move || worker(id, state))
})
.collect();
// Add jobs
{
let (lock, cvar) = &*state;
let mut queue = lock.lock();
for i in 0..10 {
queue.jobs.push_back(format!("Job {}", i));
}
cvar.notify_all(); // Wake all workers
}
// Shutdown
thread::sleep(Duration::from_secs(2));
{
let (lock, cvar) = &*state;
let mut queue = lock.lock();
queue.shutdown = true;
cvar.notify_all(); // Wake workers to check shutdown
}
// Wait for workers
for worker in workers {
worker.join().unwrap();
}
println!("All workers finished");
}A complete job queue using wait_until for clean condition handling.
Summary Table
fn summary() {
// | Method | Pattern | Returns |
// |-----------------|--------------------------------|-----------------|
// | wait | Manual while loop required | () |
// | wait_until | Predicate closure, auto-loop | MutexGuard |
// | wait_timeout | Manual loop + timeout check | WaitTimeoutResult |
// | wait_until(deadline, pred) | Predicate + timeout | WaitUntilResult |
// | Feature | wait() | wait_until() |
// |--------------------------|------------|--------------|
// | Spurious wakeup safe | Manual | Automatic |
// | Predicate required | No | Yes |
// | Timeout support | wait_timeout | wait_until(deadline, pred) |
// | Common bug prevention | No | Yes |
// | Code clarity | Lower | Higher |
}Synthesis
Quick reference:
use parking_lot::{Mutex, Condvar};
let pair = Arc::new((Mutex::new(0u32), Condvar::new()));
let (lock, cvar) = &*pair;
let mut value = lock.lock();
// Manual pattern with wait()
while *value < 10 {
cvar.wait(&mut value);
}
// Equivalent with wait_until()
cvar.wait_until(&mut value, |v| *v >= 10);
// With timeout
use std::time::{Duration, Instant};
let deadline = Instant::now() + Duration::from_secs(5);
let result = cvar.wait_until(&mut value, deadline, |v| *v >= 10);
if result.is_success() {
// Condition met within timeout
}Key insight: wait_until exists because condition variable programming is error-proneāthe while !predicate { wait() } pattern must be used to guard against spurious wakeups, but it's easy to forget or write incorrectly as if. wait_until encapsulates this pattern, taking a predicate closure that returns true when waiting should stop. The implementation is simple: check the predicate, if false then wait and re-check. This guarantees the predicate holds when wait_until returns, eliminating a whole class of bugs. The method also composes naturally with timeouts, returning a result that indicates whether the condition was met or time elapsed. Use wait when you need fine-grained control over the loop (complex state transitions, multiple conditions that change independently), but default to wait_until for clarity and correctnessāthe performance is identical, but the safety is higher.
