shuttle_std/sync/condvar.rs
1use crate::sync::{MutexGuard, ResourceSignature, ResourceType};
2use assoc::AssocExt;
3use shuttle_engine::current;
4use shuttle_engine::runtime::execution::ExecutionState;
5use shuttle_engine::runtime::task::clock::VectorClock;
6use shuttle_engine::runtime::task::TaskId;
7use shuttle_engine::runtime::thread;
8use std::cell::RefCell;
9use std::collections::VecDeque;
10use std::sync::{LockResult, PoisonError};
11use std::time::Duration;
12use tracing::trace;
13
14/// A `Condvar` represents the ability to block a thread such that it consumes no CPU time while
15/// waiting for an event to occur.
16#[derive(Debug)]
17pub struct Condvar {
18 state: RefCell<CondvarState>,
19 #[allow(unused)]
20 signature: ResourceSignature,
21}
22
23#[derive(Debug)]
24struct CondvarState {
25 // TODO: this should be a HashMap but [HashMap::new] is not const
26 waiters: Vec<(TaskId, CondvarWaitStatus)>,
27 next_epoch: usize,
28}
29
30// For tracking causal dependencies, we record the clock C of the thread that does the notify.
31// When a thread is unblocked, its clock is updated by C.
32#[derive(PartialEq, Eq, Debug)]
33enum CondvarWaitStatus {
34 Waiting,
35 // invariant: VecDeque is non-empty (if it's empty, we should be Waiting instead)
36 Signal(VecDeque<(usize, VectorClock)>),
37 Broadcast(VectorClock),
38}
39
40// TODO Check if we can avoid using epochs now that we have vector clocks.
41// TODO See [Issue 39](https://github.com/awslabs/shuttle/issues/39)
42
43// We implement `Condvar` by tracking the `CondvarWaitStatus` of each thread currently waiting on
44// the `Condvar`.
45//
46// ## Terminology
47//
48// There's competing notions of "unblocked" here -- unblocked *from the condition variable*, which
49// is the API-level notion of blocked, and unblocked *within Shuttle's scheduler*, which is an
50// implementation detail. To disambiguate, we'll call the latter "runnable" or "unrunnable", even
51// though that's a little awkward.
52//
53// ## `notify_one`
54//
55// A `notify_one` unblocks *one* currently blocked thread. We want the scheduler to be able to
56// choose which thread that is, and so we implement `notify_one` by marking all waiters as runnable.
57// Whichever waiter wins the race by running first will mark all other waiters as unrunnable again,
58//
59// This gets a little hairy if there are racing `wait`ers and `notify_one`s. The scenario we're
60// concerned about is this:
61//
62// Thread 1 | Thread 2 | Thread 3 | Thread 4 | Thread 5
63// ---------------|----------------|----------------|----------------| ----------------
64// (1) wait() | | | |
65// (2) | wait() | | |
66// (3) | | notify_one() | |
67// (4) | | | wait() |
68// (5) | | | | wait()
69// (6) | | notify_one() | |
70// (7) | | | wake |
71//
72// Here, after (6), all 4 waiter threads are runnable. Thread 4 wins the race to run first at (7),
73// and so is chosen as the unblocked thread. After (7), Thread 5 needs to be made unrunnable,
74// because the only signal it can see was (6), which has already unblocked Thread 4. However,
75// Threads 1 and 2 need to remain runnable, because they are eligible to be unblocked by (3), which
76// has not yet been consumed.
77//
78// We solve this problem with "epochs". Each `notify_one` is associated with a unique epoch, and
79// each waiter in the `CondvarState` remembers a list of the epochs that occurred while it was
80// blocked. When a waiter runs after being made runnable by a `notify_one`, it checks to see which
81// epoch that notify was associated with, and removes that epoch from the lists of every other
82// waiter. Any waiter that still has a non-empty list of epochs should remain runnable, because
83// there are still signals it's eligible to receive. Any waiter with an empty list of epochs is made
84// unrunnable, because all the signals it was present for have been consumed.
85//
86// In the scenario above, there are two epochs (3) and (6). At (7), Threads 1 and 2 have the same
87// epoch list [0, 1], and Threads 4 and 5 have the same epoch list [1]. When Thread 4 wins the race,
88// it sees that it was woken by epoch 1, and so removes that epoch from all other waiter lists.
89// Thread 1 and 2 still have epoch 0 in their list, so they remain runnable. Thread 5 has no more
90// epochs in its list, so it is made unrunnable. Whichever of Threads 1 and 2 wins the subsequent
91// race (not shown) will observe that it was woken by epoch 0, remove epoch 0 from whichever of the
92// two threads lost the race, and then make that thread unrunnable again because there are no
93// signals remaining for it to observe.
94//
95// ## `notify_all`
96//
97// `notify_all` is a broadcast that unblocks *all* currently blocked threads. Once a `notify_all`
98// occurs, all the state discussed above is irrelevant -- every thread should be unblocked, so we
99// don't need to remember whether there is also a `notify_one` that could have unblocked them.
100// Waiters that arrive after the `notify_all` will be blocked as usual.
101//
102// `notify_all` atomically unblocks all currently blocked threads. For example, consider:
103//
104// Thread 1 | Thread 2 | Thread 3 | Thread 4
105// ---------------|----------------|----------------|----------------
106// (1) wait() | | |
107// (2) | wait() | |
108// (3) | | notify_all() |
109// (4) | | | wait()
110// (5) | | notify_one() |
111//
112// After (3), Threads 1 and 2 are considered unblocked, even though the scheduler has not yet run
113// them again, Thread 4 becomes blocked at (4). At (5), the only blocked thread is Thread 4, because
114// the others were unblocked by (3), even though they have not yet woken up to discover this fact.
115// In other words, this execution cannot deadlock -- if (4) happens-before (5), then Thread 4 is
116// guaranteed to be the thread unblocked by (5). After (5), Threads 1, 2, and 4 are all runnable,
117// and can run in any order (because they are all contending on the same mutex).
118impl Condvar {
119 /// Creates a new condition variable which is ready to be waited on and notified.
120 #[track_caller]
121 pub const fn new() -> Self {
122 let state = CondvarState {
123 waiters: Vec::new(),
124 next_epoch: 0,
125 };
126
127 Self {
128 state: RefCell::new(state),
129 signature: ResourceSignature::new_const(ResourceType::Condvar),
130 }
131 }
132
133 /// Blocks the current thread until this condition variable receives a notification.
134 pub fn wait<'a, T>(&self, guard: MutexGuard<'a, T>) -> LockResult<MutexGuard<'a, T>> {
135 let me = ExecutionState::me();
136
137 // Release the lock, which allows for a switch *before* unlocking, but not after
138 // This is because the MutexGuard internally calls `batch_semaphore::release` when it
139 // unlocks via it's drop handler. `release` itself is a visible operation, so it provides
140 // it's own scheduling point prior to releasing the mutex. As all scheduling points are
141 // *only before* visible operations, anything done after this point is not visible until
142 // we switch ourselves
143 let mutex = guard.unlock();
144 // Unlocked, but no other task has run yet. We thus block ourselves and switch
145 let mut state = self.state.borrow_mut();
146
147 trace!(waiters=?state.waiters, next_epoch=state.next_epoch, "waiting on condvar {:p}", self);
148
149 debug_assert!(<_ as AssocExt<_, _>>::get(&state.waiters, &me).is_none());
150 state.waiters.push((me, CondvarWaitStatus::Waiting));
151 drop(state);
152
153 // If execution teardown unwinds the task from the switch below (see
154 // `ExecutionState::tear_down`), the task is no longer waiting. Its destructors run as the task,
155 // and may notify this condvar.
156 struct StopWaitingOnUnwind<'a>(&'a Condvar, TaskId);
157 impl Drop for StopWaitingOnUnwind<'_> {
158 fn drop(&mut self) {
159 <_ as AssocExt<_, _>>::remove(&mut self.0.state.borrow_mut().waiters, &self.1);
160 }
161 }
162 let stop_waiting_on_unwind = StopWaitingOnUnwind(self, me);
163
164 // TODO: Condvar::wait should allow for spurious wakeups.
165 ExecutionState::with(|s| s.current_mut().block(false));
166 thread::switch();
167 std::mem::forget(stop_waiting_on_unwind);
168
169 // After the context switch, consume whichever signal that woke this thread
170 let mut state = self.state.borrow_mut();
171 trace!(waiters=?state.waiters, next_epoch=state.next_epoch, "woken from condvar {:p}", self);
172 let my_status = <_ as AssocExt<_, _>>::remove(&mut state.waiters, &me).expect("should be waiting");
173 match my_status {
174 CondvarWaitStatus::Broadcast(clock) => {
175 // Woken by a broadcast, so nothing to do except update the clock
176 ExecutionState::with(|s| s.update_clock(&clock));
177 }
178 CondvarWaitStatus::Signal(mut epochs) => {
179 let (epoch, clock) = epochs.pop_front().expect("should be a pending signal");
180 // No other waiter is allowed to be unblocked by the epoch that woke us
181 for (tid, status) in state.waiters.iter_mut() {
182 if let CondvarWaitStatus::Signal(epochs) = status {
183 if let Some(i) = epochs.iter().position(|e| epoch == e.0) {
184 epochs.remove(i);
185 if epochs.is_empty() {
186 *status = CondvarWaitStatus::Waiting;
187 // Make the task unrunnable if there are no pending signals that
188 // could unblock it
189 // TODO: Condvar::wait should allow for spurious wakeups.
190 ExecutionState::with(|s| s.get_mut(*tid).block(false));
191 }
192 }
193 }
194 }
195 // Update the thread's clock with the clock from the notifier
196 ExecutionState::with(|s| s.update_clock(&clock));
197 }
198 CondvarWaitStatus::Waiting => panic!("should not have been woken while in Waiting status"),
199 }
200 drop(state);
201
202 // Reacquire the lock
203 // TODO The context switch involved here might be redundant? The scheduler implicitly chose
204 // TODO this thread to win the lock when it ran us after the context switch above.
205 mutex.lock()
206 }
207
208 /// Blocks the current thread until this condition variable receives a notification and the
209 /// provided condition is false.
210 pub fn wait_while<'a, T, F>(&self, mut guard: MutexGuard<'a, T>, mut condition: F) -> LockResult<MutexGuard<'a, T>>
211 where
212 F: FnMut(&mut T) -> bool,
213 {
214 while condition(&mut *guard) {
215 guard = self.wait(guard)?;
216 }
217 Ok(guard)
218 }
219
220 /// Waits on this condition variable for a notification, timing out after a specified duration.
221 pub fn wait_timeout<'a, T>(
222 &self,
223 guard: MutexGuard<'a, T>,
224 _dur: Duration,
225 ) -> LockResult<(MutexGuard<'a, T>, WaitTimeoutResult)> {
226 // TODO support the timeout case -- this method never times out
227 self.wait(guard)
228 .map(|guard| (guard, WaitTimeoutResult(false)))
229 .map_err(|e| PoisonError::new((e.into_inner(), WaitTimeoutResult(false))))
230 }
231
232 /// Waits on this condition variable for a notification, timing out after a specified duration.
233 ///
234 /// The semantics of this function are equivalent to [`wait_while`](Self::wait_while) except
235 /// that the thread will be blocked for roughly no longer than `dur`.
236 pub fn wait_timeout_while<'a, T, F>(
237 &self,
238 guard: MutexGuard<'a, T>,
239 _dur: Duration,
240 condition: F,
241 ) -> LockResult<(MutexGuard<'a, T>, WaitTimeoutResult)>
242 where
243 F: FnMut(&mut T) -> bool,
244 {
245 // TODO support the timeout case -- this method never times out
246 self.wait_while(guard, condition)
247 .map(|guard| (guard, WaitTimeoutResult(false)))
248 .map_err(|e| PoisonError::new((e.into_inner(), WaitTimeoutResult(false))))
249 }
250
251 /// Wakes up one blocked thread on this condvar.
252 ///
253 /// If there is a blocked thread on this condition variable, then it will be woken up from its
254 /// call to wait or wait_timeout. Calls to notify_one are not buffered in any way.
255 pub fn notify_one(&self) {
256 thread::switch();
257
258 let me = ExecutionState::me();
259
260 let mut state = self.state.borrow_mut();
261
262 trace!(waiters=?state.waiters, next_epoch=state.next_epoch, "notifying one on condvar {:p}", self);
263
264 let epoch = state.next_epoch;
265 for (tid, status) in state.waiters.iter_mut() {
266 assert_ne!(*tid, me);
267
268 let clock = current::clock();
269 match status {
270 CondvarWaitStatus::Waiting => {
271 let mut epochs = VecDeque::new();
272 epochs.push_back((epoch, clock));
273 *status = CondvarWaitStatus::Signal(epochs);
274 }
275 CondvarWaitStatus::Signal(epochs) => {
276 epochs.push_back((epoch, clock));
277 }
278 CondvarWaitStatus::Broadcast(_) => {
279 // no-op, broadcast will already unblock this task
280 }
281 }
282
283 // Note: the task might have been unblocked by a previous signal
284 ExecutionState::with(|s| s.get_mut(*tid).unblock());
285 }
286 state.next_epoch += 1;
287
288 drop(state);
289 }
290
291 /// Wakes up all blocked threads on this condvar.
292 pub fn notify_all(&self) {
293 thread::switch();
294
295 let me = ExecutionState::me();
296
297 let mut state = self.state.borrow_mut();
298
299 trace!(waiters=?state.waiters, next_epoch=state.next_epoch, "notifying all on condvar {:p}", self);
300
301 for (tid, status) in state.waiters.iter_mut() {
302 assert_ne!(*tid, me);
303 *status = CondvarWaitStatus::Broadcast(current::clock());
304 // Note: the task might have been unblocked by a previous signal
305 ExecutionState::with(|s| s.get_mut(*tid).unblock());
306 }
307
308 drop(state);
309 }
310}
311
312// Safety: Condvar is never actually passed across true threads, only across continuations. The
313// Rc<RefCell<_>> type therefore can't be preempted mid-bookkeeping-operation.
314// TODO we shouldn't need to do this, but RefCell is not Send
315unsafe impl Send for Condvar {}
316unsafe impl Sync for Condvar {}
317
318impl Default for Condvar {
319 fn default() -> Self {
320 Self::new()
321 }
322}
323
324/// A type indicating whether a timed wait on a condition variable returned due to a time out or not.
325#[derive(Debug, PartialEq, Eq, Copy, Clone)]
326pub struct WaitTimeoutResult(bool);
327
328impl WaitTimeoutResult {
329 /// Returns `true` if the wait was known to have timed out.
330 pub fn timed_out(&self) -> bool {
331 self.0
332 }
333}
334
335#[cfg(test)]
336mod tests {
337 use super::*;
338
339 #[test]
340 fn unique_resource_signature_condvar() {
341 shuttle_schedulers::check_random(
342 || {
343 let condvar1 = Condvar::new();
344 let condvar2 = Condvar::new();
345 assert_ne!(condvar1.signature, condvar2.signature);
346 },
347 1,
348 );
349 }
350}