Loading page…
Rust walkthroughs
Loading page…
parking_lot::Condvar::wait_until differ from standard wait for condition-based synchronization?parking_lot::Condvar::wait_until differs from the standard library's condition variable wait methods by accepting a closure-based condition predicate that it evaluates internally, eliminating the need for manual while-loop patterns and reducing the risk of spurious wakeup bugs. With std::sync::Condvar, you must write while !condition { condvar.wait(lock).unwrap(); } to handle spurious wakeups correctly, but wait_until encapsulates this pattern: it waits, wakes, re-evaluates your predicate, and continues waiting if the condition is still false. This difference in API design has significant implications: the standard library's approach relies on programmer discipline to always check conditions in a loop, while parking_lot's approach makes the correct pattern the default, preventing bugs where developers forget the loop or place the condition check incorrectly. Beyond this, parking_lot::Condvar also integrates with parking_lot::Mutex for better performance, avoids mutex poisoning, and provides additional methods like wait_until_with_timeout that combine predicate checking with deadline-based waiting.
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);
// Spawn a thread that will signal the condition
thread::spawn(move || {
let (lock, cvar) = &*pair_clone;
thread::sleep(std::time::Duration::from_millis(100));
let mut started = lock.lock().unwrap();
*started = true;
cvar.notify_one();
});
// Standard library pattern: MUST use while loop
let (lock, cvar) = &*pair;
let mut started = lock.lock().unwrap();
// This while loop is REQUIRED for correctness
// Spurious wakeups can occur without notify
while !*started {
started = cvar.wait(started).unwrap();
}
println!("Condition met!");
}The standard library requires manual loop handling for correctness.
use std::sync::{Arc, Mutex, Condvar};
use std::thread;
fn main() {
// Spurious wakeups are a real phenomenon
// A thread can wake from wait() without notify being called
// WRONG: Using if instead of while
// This bug is subtle and hard to reproduce
fn buggy_wait(pair: Arc<(Mutex<bool>, Condvar)>) {
let (lock, cvar) = &*pair;
let mut flag = lock.lock().unwrap();
// BUG: Should be while, not if
if !*flag {
flag = cvar.wait(flag).unwrap();
}
// If spurious wakeup occurs, we proceed with flag = false!
}
// CORRECT: Always use while loop
fn correct_wait(pair: Arc<(Mutex<bool>, Condvar)>) {
let (lock, cvar) = &*pair;
let mut flag = lock.lock().unwrap();
// Correctly handles spurious wakeups
while !*flag {
flag = cvar.wait(flag).unwrap();
}
}
// The standard library design forces you to write this pattern
// parking_lot's wait_until encapsulates it
}Spurious wakeups require the condition to be rechecked after every wakeup.
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;
thread::sleep(std::time::Duration::from_millis(100));
let mut started = lock.lock();
*started = true;
cvar.notify_one();
});
let (lock, cvar) = &*pair;
let started = lock.lock();
// wait_until handles the while loop internally
// Pass a closure that returns true when condition is met
cvar.wait_until(started, |flag| *flag);
println!("Condition met!");
// No need to manually write the loop
// No risk of forgetting the loop or using 'if' instead
}wait_until encapsulates the while-loop pattern with a closure predicate.
use std::sync::Arc;
use std::thread;
fn main() {
// Standard library approach
fn std_pattern() {
use std::sync::{Mutex, Condvar};
let pair = Arc::new((Mutex::new(0u32), Condvar::new()));
let pair_clone = Arc::clone(&pair);
thread::spawn(move || {
let (lock, cvar) = &*pair_clone;
let mut value = lock.lock().unwrap();
*value = 42;
cvar.notify_one();
});
let (lock, cvar) = &*pair;
let mut value = lock.lock().unwrap();
// Manual while loop required
while *value < 10 {
value = cvar.wait(value).unwrap();
}
}
// parking_lot approach
fn parking_lot_pattern() {
use parking_lot::{Mutex, Condvar};
let pair = Arc::new((Mutex::new(0u32), Condvar::new()));
let pair_clone = Arc::clone(&pair);
thread::spawn(move || {
let (lock, cvar) = &*pair_clone;
let mut value = lock.lock();
*value = 42;
cvar.notify_one();
});
let (lock, cvar) = &*pair;
let value = lock.lock();
// Single call with predicate
cvar.wait_until(value, |v| *v >= 10);
}
println!("Both approaches demonstrated");
}wait_until reduces boilerplate and prevents common bugs.
use parking_lot::{Mutex, Condvar};
use std::sync::Arc;
use std::thread;
fn main() {
let state = Arc::new((Mutex::new(Vec::new()), Condvar::new()));
let state_clone = Arc::clone(&state);
// Producer thread
thread::spawn(move || {
let (lock, cvar) = &*state_clone;
for i in 0..5 {
thread::sleep(std::time::Duration::from_millis(50));
let mut items = lock.lock();
items.push(i);
cvar.notify_one();
}
});
// Consumer waits for at least 3 items
{
let (lock, cvar) = &*state;
let items = lock.lock();
// wait_until re-evaluates predicate after each wakeup
cvar.wait_until(items, |items| items.len() >= 3);
println!("Got {} items", items.len());
}
// Complex conditions are readable
{
let (lock, cvar) = &*state;
let items = lock.lock();
// Wait until we have items AND the first is 0
cvar.wait_until(items, |items| {
!items.is_empty() && items[0] == 0
});
}
}The predicate closure can express arbitrary conditions.
use parking_lot::{Mutex, Condvar};
use std::sync::Arc;
fn main() {
let state = Arc::new((Mutex::new(0i32), Condvar::new()));
let (lock, cvar) = &*state;
// wait_until returns a MutexGuard
let value = cvar.wait_until(lock.lock(), |v| *v >= 0);
// Since predicate is immediately true, no waiting occurs
// value is a MutexGuard we can use
println!("Value: {}", *value);
// With complex conditions, the returned guard lets us inspect
let data = Arc::new((Mutex::new((0u32, false)), Condvar::new()));
let (lock, cvar) = &*data;
let result = cvar.wait_until(lock.lock(), |(count, ready)| {
*count >= 10 || *ready
});
// Check which condition was met
let (count, ready) = &*result;
if *ready {
println!("Was signaled ready with count {}", count);
} else {
println!("Count reached {}", count);
}
}wait_until returns the MutexGuard for further inspection.
use parking_lot::{Mutex, Condvar};
use std::sync::Arc;
use std::time::{Duration, Instant};
fn main() {
let state = Arc::new((Mutex::new(false), Condvar::new()));
let (lock, cvar) = &*state;
// wait_until_with_timeout: wait with timeout
let result = cvar.wait_until_with_timeout(
lock.lock(),
|flag| *flag,
Duration::from_millis(100)
);
// Returns true if condition was met, false if timeout
if result {
println!("Condition met before timeout");
} else {
println!("Timed out waiting for condition");
}
// wait_until_with_deadline: wait until specific instant
let deadline = Instant::now() + Duration::from_millis(100);
let result = cvar.wait_until_with_deadline(
lock.lock(),
|flag| *flag,
deadline
);
if result {
println!("Condition met before deadline");
} else {
println!("Deadline passed");
}
}parking_lot provides timeout variants that integrate with predicates.
use std::sync::{Arc, Mutex, Condvar};
use std::time::{Duration, Instant};
fn main() {
let state = Arc::new((Mutex::new(false), Condvar::new()));
let (lock, cvar) = &*state;
// Standard library: wait_timeout + manual loop
let mut guard = lock.lock().unwrap();
let timeout = Duration::from_millis(100);
let start = Instant::now();
// Must manually track time and check condition
while !*guard {
let elapsed = start.elapsed();
if elapsed >= timeout {
println!("Timeout!");
break;
}
let remaining = timeout - elapsed;
let result = cvar.wait_timeout(guard, remaining).unwrap();
guard = result.0;
}
// This is error-prone:
// - Must track elapsed time
// - Must calculate remaining time
// - Must check timeout AND condition
// - Easy to get wrong
println!("Standard library timeout pattern is verbose");
}The standard library requires manual timeout tracking with condition checking.
use parking_lot::{Mutex, Condvar};
use std::sync::Arc;
use std::thread;
fn main() {
let state = Arc::new((Mutex::new(0), Condvar::new()));
let (lock, cvar) = &*state;
// parking_lot Mutex does not use poisoning
// If a thread panics while holding the lock:
// - The lock is released
// - No poison error is returned
// - Other threads can continue
// Standard library:
// let guard = lock.lock().unwrap(); // Can panic on poison
// parking_lot:
let guard = lock.lock(); // No unwrap needed, no poisoning
// wait_until also doesn't need unwrap
cvar.wait_until(guard, |v| *v > 0);
// This simplifies error handling significantly
// The trade-off: data might be in inconsistent state after panic
// But for many use cases, this is acceptable
println!("No poisoning in parking_lot");
}parking_lot eliminates mutex poisoning for simpler error handling.
use parking_lot::{Mutex, Condvar};
use std::sync::Arc;
use std::collections::VecDeque;
use std::thread;
struct BoundedQueue<T> {
data: Mutex<VecDeque<T>>,
not_empty: Condvar,
not_full: Condvar,
capacity: usize,
}
impl<T> BoundedQueue<T> {
fn new(capacity: usize) -> Self {
Self {
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
self.not_full.wait_until(&mut data, |d| d.len() < self.capacity);
data.push_back(item);
self.not_empty.notify_one();
}
fn pop(&self) -> T {
let mut data = self.data.lock();
// Wait until there's an item
self.not_empty.wait_until(&mut data, |d| !d.is_empty());
let item = data.pop_front().unwrap();
self.not_full.notify_one();
item
}
}
fn main() {
let queue = Arc::new(BoundedQueue::new(5));
// Producer
let producer_queue = Arc::clone(&queue);
let producer = thread::spawn(move || {
for i in 0..20 {
producer_queue.push(i);
println!("Produced: {}", i);
}
});
// Consumer
let consumer_queue = Arc::clone(&queue);
let consumer = thread::spawn(move || {
for _ in 0..20 {
let item = consumer_queue.pop();
println!("Consumed: {}", item);
}
});
producer.join().unwrap();
consumer.join().unwrap();
}wait_until makes the producer-consumer pattern clean and correct.
fn main() {
// Aspect | std::sync::Condvar | parking_lot::Condvar
// --------------------|----------------------------|----------------------------
// Wait pattern | Manual while loop | wait_until with predicate
// Spurious wakeups | Must handle manually | Handled by wait_until
// Mutex poisoning | Yes (unwrap needed) | No (simpler API)
// Timeout handling | wait_timeout + manual loop | wait_until_with_timeout
// Lock return | LockResult<MutexGuard> | MutexGuard directly
// Condition check | Manual in loop body | Closure predicate
println!("Comparison documented above");
}API comparison:
| Pattern | std::sync::Condvar | parking_lot::Condvar |
|---------|---------------------|------------------------|
| Wait for condition | while !cond { cvar.wait(guard).unwrap(); } | cvar.wait_until(guard, \|g| cond); |
| Wait with timeout | Manual loop with wait_timeout | wait_until_with_timeout |
| Error handling | Must handle PoisonError | No poisoning |
| Boilerplate | High | Low |
When to use each:
| Situation | Recommended Choice |
|-----------|-------------------|
| New project | parking_lot::Condvar |
| Existing std codebase | std::sync::Condvar (consistency) |
| Need std compatibility | std::sync::Condvar |
| Want simpler API | parking_lot::Condvar |
| Performance critical | parking_lot::Condvar (faster mutex) |
| Teaching/learning | Start with std, show parking_lot after |
Key insight: The fundamental difference between parking_lot::Condvar::wait_until and the standard library's wait is about where the condition-checking loop lives: in your code or in the library. With std::sync::Condvar, you must remember to write while !condition { wait() } every time—this is a correctness requirement, not an optimization. The pattern is so universal that forgetting it is a bug, yet the API doesn't prevent you from forgetting. parking_lot::Condvar::wait_until inverts this: the library takes responsibility for the loop, and you provide only the condition. This makes the correct behavior the default and eliminates an entire class of bugs (using if instead of while, forgetting to check after wakeup, checking the wrong variable). The predicate-based API is also more expressive: the closure can capture context, compute derived conditions, or check multiple variables, all while the library handles the synchronization mechanics. Combined with parking_lot's faster mutex implementation, no-poisoning design, and integrated timeout methods, wait_until represents a more ergonomic and safer approach to condition-based synchronization—but at the cost of an external dependency and deviation from the standard library patterns that Rust developers learn first.