Synchronization Primitives
Condvar
std::sync::Condvar (Condition Variable) is a synchronization primitive that allows a thread to sleep (consuming zero CPU cycles) while atomically releasing a MutexGuard<'_, T> until another thread mutates the state inside the Mutex<T> and notifies the Condvar.
How Condvar::wait Atomically Unlocks and Re-Locks a MutexGuard
A Condvar is always paired with a Mutex<T> ((Mutex<State>, Condvar)). Calling guard = cvar.wait(guard).unwrap() performs three steps:
- Atomically unlocks
guardand puts the calling thread to sleep in the kernel wait queue (preventing any "lost wakeup" window between checking the condition and going to sleep!). - Waits until another thread calls
cvar.notify_one()orcvar.notify_all(). - Re-acquires the
Mutexlock before returning the freshMutexGuard<'_, T>to the caller!
Waiting Thread (`cvar.wait_while`) Notifying Thread (`cvar.notify_one`)
---------------------------------- ------------------------------------
1. Locks `Mutex` -> gets `guard`
2. Checks predicate (`queue.is_empty()`)
3. Calls `cvar.wait(guard)`:
- Atomically unlocks `Mutex`
- Sleeps in OS kernel .............. 4. Locks `Mutex` -> pushes item -> unlocks `Mutex`
<............. 5. Calls `cvar.notify_one()`
6. Wakes up & re-acquires `Mutex` `guard`
7. Re-checks predicate -> Proceeds!
Core Condvar Methods
| Method | Signature | Description |
|---|---|---|
Condvar::new() |
const fn new() -> Condvar |
Creates a new condition variable (const-constructible) |
.wait(guard) |
fn wait<'a, T>(&self, guard: MutexGuard<'a, T>) -> LockResult<MutexGuard<'a, T>> |
Unlocks guard, sleeps until notified (or spuriously woken), and re-locks guard |
.wait_while(guard, cond) |
fn wait_while<'a, T, F>(&self, guard: MutexGuard<'a, T>, condition: F) -> LockResult<MutexGuard<'a, T>> |
Loops calling .wait(guard) for as long as condition(&mut *guard) returns true (automatically handles spurious wakeups!) |
.wait_timeout(guard, dur) |
fn wait_timeout<'a, T>(&self, guard: MutexGuard<'a, T>, dur: Duration) -> LockResult<(MutexGuard<'a, T>, WaitTimeoutResult)> |
Waits with a timeout; .timed_out() indicates whether dur elapsed |
.notify_one() |
fn notify_one(&self) |
Wakes up one waiting thread (if any are currently waiting) |
.notify_all() |
fn notify_all(&self) |
Wakes up all waiting threads |
Why wait_while (or a while Loop) Is Mandatory Around .wait()
Operating-system condition variables are subject to spurious wakeups (a waiting thread can wake up even if no thread called notify_one(), or a third thread can steal the queued item before the woken thread re-acquires the mutex). Using cvar.wait_while(guard, |state| ...) automatically re-evaluates the condition in a loop until the desired state holds:
use std::collections::VecDeque;
use std::sync::{Condvar, Mutex};
use std::thread;
struct BoundedQueue<T> {
buffer: Mutex<VecDeque<T>>,
not_empty: Condvar,
not_full: Condvar,
capacity: usize,
}
impl<T> BoundedQueue<T> {
fn new(capacity: usize) -> Self {
Self {
buffer: Mutex::new(VecDeque::with_capacity(capacity)),
not_empty: Condvar::new(),
not_full: Condvar::new(),
capacity,
}
}
fn push(&self, item: T) {
let guard = self.buffer.lock().unwrap();
// Wait while the buffer is full (`len == capacity`):
let mut guard = self
.not_full
.wait_while(guard, |q| q.len() == self.capacity)
.unwrap();
guard.push_back(item);
drop(guard); // Unlock before notifying so the woken consumer doesn't immediately block!
self.not_empty.notify_one();
}
fn pop(&self) -> T {
let guard = self.buffer.lock().unwrap();
// Wait while the buffer is empty (`q.is_empty()`):
let mut guard = self
.not_empty
.wait_while(guard, |q| q.is_empty())
.unwrap();
let item = guard.pop_front().unwrap();
drop(guard);
self.not_full.notify_one();
item
}
}
fn main() {
let queue = BoundedQueue::new(2);
thread::scope(|s| {
s.spawn(|| {
for n in 1..=4 {
queue.push(n * 10);
}
});
for _ in 1..=4 {
println!("consumer popped: {}", queue.pop());
}
});
}
consumer popped: 10
consumer popped: 20
consumer popped: 30
consumer popped: 40