Skip to main content

shuttle_engine/runtime/thread/
continuation.rs

1use crate::config::UNGRACEFUL_SHUTDOWN_CONFIG;
2use crate::runtime::execution::ExecutionState;
3use corosensei::Yielder;
4use corosensei::{stack::DefaultStack, Coroutine, CoroutineResult};
5use scoped_tls::scoped_thread_local;
6use std::cell::{Cell, RefCell};
7use std::collections::VecDeque;
8use std::ops::Deref;
9use std::ops::DerefMut;
10use std::panic::Location;
11use std::rc::Rc;
12use tracing::trace;
13
14scoped_thread_local! {
15    pub static CONTINUATION_POOL: ContinuationPool
16}
17
18/// A continuation is a green thread that can be resumed and yielded at will. We use it to
19/// execute a "thread" from within a Future.
20///
21/// For efficiency, we reuse continuations. The continuation can be provided a new function
22/// to run via `initialize`. A continuation is only reusable if the previous function it was
23/// executing completed.
24pub struct Continuation {
25    coroutine: Coroutine<ContinuationInput, ContinuationOutput, ContinuationOutput>,
26    function: ContinuationFunction,
27    state: ContinuationState,
28    pub yielder: *const Yielder<ContinuationInput, ContinuationOutput>,
29}
30
31impl std::fmt::Debug for Continuation {
32    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
33        f.debug_struct("Continuation")
34            .field("state", &self.state)
35            .finish_non_exhaustive()
36    }
37}
38
39/// A cell to pass functions into continuations
40#[allow(clippy::type_complexity)]
41#[derive(Clone)]
42struct ContinuationFunction(Rc<Cell<Option<Box<dyn FnOnce()>>>>);
43
44// Safety: we arrange for the `function` field of `Continuation` to only be accessed by one thread
45// at a time: Shuttle tests are single threaded, and continuations are never shared across threads
46// by the ContinuationPool, which is thread-local.
47unsafe impl Send for ContinuationFunction {}
48
49/// Inputs that we can pass to a continuation.
50#[derive(Debug, PartialEq, Eq, Clone, Copy)]
51pub enum ContinuationInput {
52    Resume,
53    Exit,
54    /// Drop the function instead of running it, or stop the future it polls (see `Continuation::cancel`).
55    Cancel,
56}
57
58/// Outputs that a continuation can pass back to us
59#[derive(Debug, PartialEq, Eq, Clone, Copy)]
60pub enum ContinuationOutput {
61    Yielded,
62    Finished(*const Yielder<ContinuationInput, ContinuationOutput>),
63    Exited,
64}
65
66/// The current state of a continuation. Lifecycle runs from top to bottom.
67#[derive(Debug, PartialEq, Eq, Clone, Copy)]
68enum ContinuationState {
69    NotReady,          // has no function in its cell; waiting for input about what to do next
70    Initialized,       // has a function in its cell, but hasn't started running yet
71    Ready,             // has a suspended function in its cell; waiting for input about what to do next
72    Running,           // currently inside a user-provided function
73    FinishedIteration, // has finished the previous function, can be initialized with a new one
74    Exited,            // the internal coroutine has exited its loop and cannot receive new functions to execute
75}
76
77impl Continuation {
78    pub fn new(stack_size: usize) -> Self {
79        let function = ContinuationFunction(Rc::new(Cell::new(None)));
80
81        let mut coroutine = {
82            let function = function.clone();
83
84            Coroutine::with_stack(DefaultStack::new(stack_size).unwrap(), move |yielder, input| {
85                if let ContinuationInput::Exit = input {
86                    return ContinuationOutput::Exited;
87                }
88
89                // Move the whole `ContinuationFunction`, not just its field (Rust 2021 thing)
90                let _ = &function;
91
92                loop {
93                    // Tell the caller we've finished the previous user function (or if this is our
94                    // first time around the loop, the caller below expects us to pretend we've
95                    // finished the previous function).
96                    let input = yielder.suspend(ContinuationOutput::Finished(yielder as *const _));
97                    let f = match input {
98                        ContinuationInput::Exit => break,
99                        ContinuationInput::Resume | ContinuationInput::Cancel => {
100                            function.0.take().expect("must have a function to run")
101                        }
102                    };
103
104                    if input == ContinuationInput::Cancel {
105                        // The function never ran: drop it here, on this stack, so that the destructors
106                        // of what it owns run like a running task's (see `Continuation::cancel`).
107                        drop(f);
108                    } else {
109                        f();
110                    }
111                }
112
113                ContinuationOutput::Exited
114            })
115        };
116
117        // Resume the coroutine once to get it into the loop. Because the continuations are kept in a pool and reused,
118        // this requires us to exfiltrate the yielder from the closure passed in to Coroutine::with_stack and persist it
119        // for the lifetime of the continuation's inner function. The yielder exfiltration happens below on the first suspend
120        // of the coroutine
121        let yielder = match coroutine.resume(ContinuationInput::Resume) {
122            CoroutineResult::Yield(ContinuationOutput::Finished(yielder)) => yielder,
123            _ => panic!("Coroutine should yield a pointer to its `corosensei::Yielder` from the first resume"),
124        };
125
126        Self {
127            coroutine,
128            yielder,
129            function,
130            state: ContinuationState::NotReady,
131        }
132    }
133
134    /// Provide a new function for the continuation to execute. The continuation must
135    /// be in reusable state.
136    pub fn initialize(&mut self, fun: Box<dyn FnOnce()>) {
137        debug_assert!(self.reusable(), "shouldn't replace a function before it completes");
138
139        let old = self.function.0.replace(Some(fun));
140        debug_assert!(old.is_none(), "shouldn't replace a function before it runs");
141
142        self.state = ContinuationState::Initialized;
143    }
144
145    /// Resume the continuation, and returns true if the function it was executing has finished.
146    pub fn resume(&mut self) -> bool {
147        debug_assert!(self.state == ContinuationState::Ready || self.state == ContinuationState::Initialized);
148
149        let ret = self.resume_with_input(ContinuationInput::Resume);
150        debug_assert_ne!(
151            ret,
152            ContinuationOutput::Exited,
153            "continuation should not exit if resumed from user code"
154        );
155
156        matches!(ret, ContinuationOutput::Finished(_))
157    }
158
159    /// Resume the continuation to cancel its function, and return true if the function has
160    /// finished, like `resume`. This is how execution teardown drops a task that is unfinished at
161    /// the end of an execution, on the task's own stack (see `ExecutionState::tear_down`). A
162    /// function that never ran is dropped. A function suspended between polls of a future (see
163    /// `switch_between_polls`) drops the future and returns. Only these two are cancelled: any other
164    /// suspended function is in the middle of user code, which only unwinding its stack can stop.
165    pub fn cancel(&mut self) -> bool {
166        debug_assert!(self.state == ContinuationState::Ready || self.state == ContinuationState::Initialized);
167
168        let ret = self.resume_with_input(ContinuationInput::Cancel);
169        debug_assert_ne!(
170            ret,
171            ContinuationOutput::Exited,
172            "continuation should not exit when cancelled"
173        );
174
175        matches!(ret, ContinuationOutput::Finished(_))
176    }
177
178    /// Whether the continuation has a function that has not started running yet.
179    pub(crate) fn never_ran(&self) -> bool {
180        self.state == ContinuationState::Initialized
181    }
182
183    /// Whether the continuation's function is suspended, and can be resumed.
184    pub(crate) fn suspended(&self) -> bool {
185        self.state == ContinuationState::Ready
186    }
187
188    /// Leak the function the continuation was given, which has not started running, so that the
189    /// continuation can be reused.
190    pub(crate) fn leak_function(&mut self) {
191        debug_assert!(self.never_ran());
192        std::mem::forget(self.function.0.take());
193        self.state = ContinuationState::NotReady;
194    }
195
196    /// Unwind the stack of a continuation that is suspended in its function, which drops what is on
197    /// it, as a panic would. If the function catches the unwind, it starts again where the function
198    /// next switches (see `Coroutine::force_unwind`).
199    pub(crate) fn unwind_stack(&mut self) {
200        debug_assert!(self.state == ContinuationState::Ready);
201        self.coroutine.force_unwind();
202        self.state = ContinuationState::Exited;
203    }
204
205    /// Leak the stack of a continuation that is suspended in its function, or whose function
206    /// panicked out of it: nothing on it is dropped, and the stack is freed with the continuation.
207    pub(crate) fn leak_stack(&mut self) {
208        debug_assert!(self.state == ContinuationState::Ready || self.state == ContinuationState::Running);
209        // SAFETY: this leaks the objects on the coroutine's stack without dropping them, so anything
210        // that still refers into the stack is left dangling (see `Coroutine::force_reset`). Shuttle
211        // has accepted that for stopped and failed executions, whose tasks never run again (see
212        // `ExecutionState::tear_down`). Teardown drops the functions of the tasks that never ran,
213        // which can borrow from another task's stack (scoped threads), before it leaks any stack.
214        unsafe {
215            self.coroutine.force_reset();
216        }
217        self.state = ContinuationState::Exited;
218    }
219
220    fn resume_with_input(&mut self, input: ContinuationInput) -> ContinuationOutput {
221        self.state = ContinuationState::Running;
222        match self.coroutine.resume(input) {
223            CoroutineResult::Yield(output) => {
224                self.state = match output {
225                    ContinuationOutput::Finished(_) => ContinuationState::FinishedIteration,
226                    ContinuationOutput::Yielded => ContinuationState::Ready,
227                    ContinuationOutput::Exited => ContinuationState::Exited,
228                };
229                output
230            }
231            CoroutineResult::Return(output) => {
232                self.state = ContinuationState::Exited;
233                output
234            }
235        }
236    }
237
238    /// A continuation is reusable if it has completed running a user function and is waiting
239    /// to be initialized with a new one. A continuation isn't reusable if it's still inside the user
240    /// function `f` (Ready or Running), as changing it's inner function would leak the currently
241    /// executing context. We also do not consider Initialized coroutines to be reusable to avoid
242    /// accidentally overwriting a continuation before it has had a chance to run. Continuations which
243    /// have Exited, are not reusable as they have broken out of the loop where their inner functions
244    /// can be replaced.
245    fn reusable(&self) -> bool {
246        self.state == ContinuationState::NotReady || self.state == ContinuationState::FinishedIteration
247    }
248}
249
250impl Drop for Continuation {
251    fn drop(&mut self) {
252        // If the continuation is reusable, we tell it to exit and gracefully clean up its
253        // resources. If not, we can't send it an exit message because it might be stopped in
254        // arbitrary user code. Its resources will still be cleaned up when the underlying
255        // generator is dropped, but doing so is slower (the generator impl invokes a panic
256        // inside the continuation), so this drop handler exists to avoid it when possible.
257        match self.state {
258            ContinuationState::Initialized | ContinuationState::FinishedIteration | ContinuationState::NotReady => {
259                let ret = self.resume_with_input(ContinuationInput::Exit);
260                debug_assert_eq!(ret, ContinuationOutput::Exited);
261            }
262            ContinuationState::Running | ContinuationState::Ready => {
263                // Execution teardown unwinds or leaks the stacks of the tasks that are unfinished at
264                // the end of an execution itself (see `ExecutionState::tear_down`), so this is only
265                // reached for a continuation dropped some other way. If already panicking or the
266                // execution stopped, don't worry about cleaning up resources on individual coroutines
267                // which are still in-flight: unwinding them runs drop handlers, which can then panic
268                // if they interact with shuttle atomics, causing a panic-on-drop abort.
269                //
270                // SAFETY: `force_reset` leaks the coroutine. However, given that the execution is *already* aborting
271                // at this point and will soon exit, this is unlikely to cause issues. Leaking the coroutine here also
272                // avoids most tricky issues with scheduling points in drop handlers during a panic, which can often
273                // result in difficult-to-debug aborts from double-panics.
274                if std::thread::panicking() || ExecutionState::execution_stopped() {
275                    unsafe {
276                        self.coroutine.force_reset();
277                    }
278                } else {
279                    self.coroutine.force_unwind();
280                }
281            }
282            ContinuationState::Exited => {
283                // Already exited, nothing to do
284            }
285        }
286    }
287}
288
289/// A `ContinuationPool` just holds on to old `Continuation`s that are reusable, and vends
290/// them back out again. This amortizes the cost of allocating continuations, which involve
291/// allocating new stacks (`mmap`), `mprotect`, etc.
292pub struct ContinuationPool {
293    // invariant: if c is in this queue, c.reusable() == true
294    continuations: Rc<RefCell<VecDeque<Continuation>>>,
295}
296
297impl std::fmt::Debug for ContinuationPool {
298    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
299        f.debug_struct("ContinuationPool").finish_non_exhaustive()
300    }
301}
302
303impl ContinuationPool {
304    pub fn new() -> Self {
305        Self {
306            continuations: Rc::new(RefCell::new(VecDeque::new())),
307        }
308    }
309
310    /// Acquire a new continuation from the global pool. Panics if that pool was not yet initialized.
311    pub fn acquire(stack_size: usize) -> PooledContinuation {
312        CONTINUATION_POOL.with(|p| p.acquire_inner(stack_size))
313    }
314
315    fn acquire_inner(&self, stack_size: usize) -> PooledContinuation {
316        // TODO add a check to ensure that if we recycled a continuation, its
317        // TODO allocated stack size is at least the requested `stack_size`
318        let continuation = self
319            .continuations
320            .borrow_mut()
321            .pop_front()
322            .unwrap_or_else(move || Continuation::new(stack_size));
323
324        PooledContinuation {
325            continuation: Some(continuation),
326            queue: self.continuations.clone(),
327        }
328    }
329}
330
331/// A thin wrapper around a `Continuation` that returns it to a `ContinuationPool`
332/// when dropped, but only if it's reusable.
333pub struct PooledContinuation {
334    continuation: Option<Continuation>,
335    queue: Rc<RefCell<VecDeque<Continuation>>>,
336}
337
338impl Drop for PooledContinuation {
339    fn drop(&mut self) {
340        let mut c = self.continuation.take().unwrap();
341        if c.reusable() {
342            self.queue.borrow_mut().push_back(c);
343        } else if matches!(c.state, ContinuationState::Initialized) {
344            // A continuation which has been initialized but not run cannot be immediately reused.
345            // This is because arguments and captures may already have been moved into the function,
346            // and thus these moved objects won't be dropped until the function itself has been
347            // dropped. Thus we must drop the inner function before reusing it.
348            //
349            // Execution teardown deals with the functions of the tasks that never ran itself (see
350            // `ExecutionState::tear_down`), so this is only reached for a continuation dropped some
351            // other way.
352            if std::thread::panicking() && UNGRACEFUL_SHUTDOWN_CONFIG.get().continuation_function_behavior.leaks() {
353                c.leak_function();
354            } else {
355                drop(c.function.0.replace(None));
356                c.state = ContinuationState::NotReady;
357            }
358            self.queue.borrow_mut().push_back(c);
359        }
360    }
361}
362
363impl Deref for PooledContinuation {
364    type Target = Continuation;
365
366    fn deref(&self) -> &Self::Target {
367        self.continuation.as_ref().unwrap()
368    }
369}
370
371impl DerefMut for PooledContinuation {
372    fn deref_mut(&mut self) -> &mut Self::Target {
373        self.continuation.as_mut().unwrap()
374    }
375}
376
377impl std::fmt::Debug for PooledContinuation {
378    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
379        f.debug_struct("PooledContinuation").finish()
380    }
381}
382
383// Safety: these aren't sent across real threads
384unsafe impl Send for PooledContinuation {}
385
386/// Possibly yield back to the executor to perform a context switch.  This function should be
387/// called *before* any visible operation. If each visible operation has a scheduling point
388/// before it, then there will be a potential context switch *in between* any pair of visible
389/// operations, which is a necessary condition for completeness.
390///
391/// Putting scheduling points before visible operations, rather than after, has the advantage
392/// of giving the scheduling algorithm additional information to make scheduling decisions
393/// based on what is about to happen on each task. The disadvantage of this approach is that it
394/// is more difficult to avoid double-yields for blocking operations, explained below.
395///
396/// In addition to the scheduling point before the operation begins, blocking operations will
397/// result in a *second* context switch if the current thread is blocked. As an optimization,
398/// the switch *before* the blocking operation can be conditionally omitted to avoid switching
399/// twice for the same operation iff (1) the operation *will* block and (2) if the act of
400/// blocking *commutes* with all other operations on that resource.
401///
402/// Reasoning: We can consider a blocking operation (`Y`) such as acquiring a mutex as two
403/// sub-operations (`Y1`) blocking and (`Y2`) proceeding after being unblocked. The double-yield
404/// optimization omits the scheduling point before `Y1`. For arbitrary events `X` and `Z` and
405/// intra-thread orderings `T1: X Y1 Y2` and `T2: Z`, we have four interleavings:
406///
407/// `X Z Y1 Y2`
408/// `X Y1 Z Y2`
409/// `Z X Y1 Y2`
410/// `X Y1 Y2 Z`
411///
412/// Note that the first interleaving is *not observable* if we omit the scheduling point before `Y1`.
413/// Thus to maintain behavioral completeness when omitting this scheduling point, all states
414/// observable from the first schedule must also be observable in one of the other schedules.
415///
416/// Observe that if `Y1` and `Z` commute, then the first two schedules are behaviorally equivalent,
417/// thus the optimization is safe. So, to ensure the safety of the double-yield optimization for an
418/// operation `Y1`, it suffices to check that `Y1` commutes with all operations `Z` on the same resource,
419/// as operations on other resources should commute trivially.
420#[track_caller]
421pub fn switch() {
422    crate::annotations::record_tick();
423    trace!("switch from {}", Location::caller());
424    if ExecutionState::maybe_yield() {
425        match suspend() {
426            ContinuationInput::Exit => panic!("unexpected exit continuation"),
427            ContinuationInput::Resume => {}
428            ContinuationInput::Cancel => unreachable!("only a task that is between polls is cancelled"),
429        }
430    }
431}
432
433/// Like `switch`, for the loop that polls a future task's future (see `Task::from_future`), between
434/// two polls. Returns true if execution teardown cancelled the task while it was switched out (see
435/// `Continuation::cancel`), in which case the loop drops the future instead of polling it again.
436pub(crate) fn switch_between_polls() -> bool {
437    crate::annotations::record_tick();
438    trace!("switch between polls");
439    ExecutionState::maybe_yield() && suspend() == ContinuationInput::Cancel
440}
441
442/// Suspend the current task's continuation, and return what it is resumed with.
443#[inline(always)]
444fn suspend() -> ContinuationInput {
445    let yielder = ExecutionState::with(|state| state.current().yielder);
446    assert!(!yielder.is_null(), "a task without a stack cannot switch");
447
448    // SAFETY: A yielder reference is valid for the lifetime of the continuation (see
449    // `corosensei::Coroutine::with_stack`), and the task's `yielder` is its continuation's. A task
450    // owns its continuation until execution teardown takes it, to unwind or leak its stack, and
451    // teardown clears `yielder` once the stack is gone. `maybe_yield` only asks a task to switch
452    // while it runs on its continuation, which a task whose stack teardown unwinds still does. There
453    // are no mutable references to the yielder.
454    unsafe { &(*yielder) }.suspend(ContinuationOutput::Yielded)
455}
456
457#[cfg(test)]
458mod tests {
459    use super::*;
460    use crate::config::Config;
461
462    #[test]
463    fn reusable_continuation_drop() {
464        let pool = ContinuationPool::new();
465        let config: Config = Default::default();
466
467        let mut c = pool.acquire_inner(config.stack_size);
468        c.initialize(Box::new(|| {
469            let _ = 1 + 1;
470        }));
471        let yielder = c.yielder;
472
473        let r = c.resume();
474        assert!(r, "continuation only has one step");
475
476        drop(c);
477        assert_eq!(
478            pool.continuations.borrow().len(),
479            1,
480            "continuation should be reusable because the function finished"
481        );
482
483        let mut c = pool.acquire_inner(config.stack_size);
484        c.initialize(Box::new(move || {
485            // SAFETY: We must use the yielder directly here because we are not in a Shuttle execution
486            // for `continuation::switch`, which requires access to `ExecutionState`. This is safe because
487            // the yielder's lifetime is valid as long as the continuation has not `Exited`, and the
488            // continuation cannot have exited if it is executing its current function.
489            unsafe { &(*yielder) }.suspend(ContinuationOutput::Yielded);
490            let _ = 1 + 1;
491        }));
492
493        let r = c.resume();
494        assert!(!r, "continuation yields once, shouldn't be finished yet");
495
496        drop(c);
497        assert_eq!(
498            pool.continuations.borrow().len(),
499            0,
500            "continuation should not be reusable because the function wasn't finished"
501        );
502
503        let c = pool.acquire_inner(config.stack_size);
504
505        // Check that it's safe for a continuation to outlive the pool
506        drop(pool);
507        drop(c);
508    }
509}