Skip to main content

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}