Skip to main content

miri/concurrency/
scheduler.rs

1//! This file contains Miri's scheduler, which is in charge of picking the next thread to run when
2//! the current thread blocks/yields. It also manages timeouts and runs an async event loop
3//! to deal with host I/O. In GenMC mode, it handles delegating schedule control to GenMC.
4
5use std::cell::RefCell;
6use std::fmt;
7use std::sync::atomic::Ordering;
8use std::task::Poll;
9use std::time::Duration;
10
11use rand::seq::IteratorRandom;
12use rustc_const_eval::CTRL_C_RECEIVED;
13use rustc_index::Idx;
14use rustc_span::DUMMY_SP;
15
16use crate::*;
17
18#[derive(Clone, Copy, Debug, PartialEq)]
19enum SchedulingAction {
20    /// Execute step on the active thread.
21    ExecuteStep,
22    /// Wait for a bit, but at most as long as the duration specified.
23    /// We wake up early if an I/O event happened.
24    /// If the duration is [`None`], we sleep indefinitely. This is
25    /// only allowed when isolation is disabled and there are threads waiting for I/O!
26    SleepAndWaitForIo(Option<Duration>),
27}
28
29/// When we need to perform an operation but don't have an `ecx` around to run it immediately, we
30/// can register a callback here and it will be invoked on the next scheduler call.
31#[derive(Default)]
32pub struct DelayedCallbacks {
33    callbacks: RefCell<Vec<DelayedCallback>>,
34}
35
36pub type DelayedCallback = Box<dyn for<'tcx> FnOnce(&mut MiriInterpCx<'tcx>) -> InterpResult<'tcx>>;
37
38impl fmt::Debug for DelayedCallbacks {
39    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
40        f.debug_struct("DelayedCallbacks").field("callbacks", &"_").finish()
41    }
42}
43
44impl DelayedCallbacks {
45    /// Register a callback to be invoked the next time the current thread yields/blocks.
46    pub fn add(&self, callback: DelayedCallback) {
47        self.callbacks.borrow_mut().push(callback);
48    }
49
50    fn process<'tcx>(ecx: &mut MiriInterpCx<'tcx>) -> InterpResult<'tcx> {
51        loop {
52            // Avoid keeping the RefCell open over the callback.
53            let Some(callback) = ecx.machine.delayed_callbacks.callbacks.borrow_mut().pop() else {
54                return interp_ok(());
55            };
56            (callback)(ecx)?;
57        }
58    }
59}
60
61impl<'tcx> EvalContextPrivExt<'tcx> for MiriInterpCx<'tcx> {}
62trait EvalContextPrivExt<'tcx>: MiriInterpCxExt<'tcx> {
63    #[inline]
64    fn run_on_stack_empty(&mut self) -> InterpResult<'tcx, Poll<()>> {
65        let ecx = self.eval_context_mut();
66        let active_thread = ecx.active_thread_mut();
67        active_thread.origin_span = DUMMY_SP; // reset, the old value no longer applies
68        let mut callback = active_thread
69            .on_stack_empty
70            .take()
71            .expect("`on_stack_empty` not set up, or already running");
72        let res = callback(ecx)?;
73        ecx.active_thread_mut().on_stack_empty = Some(callback);
74        interp_ok(res)
75    }
76
77    /// Decide which action to take next and on which thread.
78    ///
79    /// The currently implemented scheduling policy is the one that is commonly
80    /// used in stateless model checkers such as Loom: run the active thread as
81    /// long as we can and switch only when we have to (the active thread was
82    /// blocked, terminated, or has explicitly asked to be preempted).
83    ///
84    /// If GenMC mode is active, the scheduling is instead handled by GenMC.
85    fn schedule(&mut self) -> InterpResult<'tcx, SchedulingAction> {
86        let ecx = self.eval_context_mut();
87
88        // In GenMC mode, we let GenMC do the scheduling.
89        if ecx.machine.data_race.as_genmc_ref().is_some() {
90            loop {
91                let genmc_ctx = ecx.machine.data_race.as_genmc_ref().unwrap();
92                let Some(next_thread_id) = genmc_ctx.schedule_thread(ecx)? else {
93                    return interp_ok(SchedulingAction::ExecuteStep);
94                };
95                // If a thread is blocked on GenMC, we have to implicitly unblock it when it gets scheduled again.
96                if ecx.machine.threads.thread_ref(next_thread_id).is_blocked_on(BlockReason::Genmc)
97                {
98                    info!(
99                        "GenMC: scheduling blocked thread {next_thread_id:?}, so we unblock it now."
100                    );
101                    ecx.unblock_thread(next_thread_id, BlockReason::Genmc)?;
102                }
103                // The thread we just unblocked may have been blocked again during the unblocking callback.
104                // In that case, we need to ask for a different thread to run next.
105                let thread_manager = &mut ecx.machine.threads;
106                if thread_manager.thread_ref(next_thread_id).is_enabled() {
107                    // Set the new active thread.
108                    thread_manager.set_active_thread(next_thread_id);
109                    return interp_ok(SchedulingAction::ExecuteStep);
110                }
111            }
112        }
113
114        // We are not in GenMC mode, so we control the scheduling.
115        let thread_manager = &ecx.machine.threads;
116        // Check if we can just keep running the current thread.
117        if thread_manager.active_thread_ref().is_enabled() && !thread_manager.yield_active_thread {
118            // The currently active thread is still enabled, just continue with it.
119            return interp_ok(SchedulingAction::ExecuteStep);
120        }
121
122        // The active thread yielded or got terminated. Let's see if there are any I/O events
123        // or timeouts to take care of.
124
125        // There may be delayed callbacks we have to process, which may unblock more threads.
126        DelayedCallbacks::process(ecx)?;
127
128        if ecx.machine.communicate() {
129            // When isolation is disabled we need to check for events for threads
130            // which are blocked on host I/O. Unlike the `poll_and_unblock` before
131            // any foreign item, the call here is needed to ensure that threads which
132            // are blocked on host I/O are woken up even if no shimmed functions are
133            // executed afterwards.
134            // We do this before running any other threads such that the threads
135            // which received events are available for scheduling afterwards.
136
137            // Perform a non-blocking poll for newly available I/O events from the OS.
138            ecx.poll_and_unblock(Some(Duration::ZERO))?;
139        }
140
141        // We also check timeouts before running any other thread, to ensure that timeouts
142        // "in the past" fire before any other thread can take an action. This ensures that for
143        // `pthread_cond_timedwait`, "an error is returned if [...] the absolute time specified by
144        // abstime has already been passed at the time of the call".
145        // <https://pubs.opengroup.org/onlinepubs/9699919799/functions/pthread_cond_timedwait.html>
146        let potential_sleep_time = ecx.unblock_expired_deadlines()?;
147
148        let thread_manager = &mut ecx.machine.threads;
149        let rng = ecx.machine.rng.get_mut();
150
151        // No callbacks immediately scheduled, pick a regular thread to execute.
152        // The active thread blocked or yielded. So we go search for another enabled thread.
153        // We build the list of threads by starting with the threads after the current one, followed by
154        // the threads before the current one and then the current thread itself (i.e., this iterator acts
155        // like `threads.rotate_left(self.active_thread.index() + 1)`. This ensures that if we pick the first
156        // eligible thread, we do regular round-robin scheduling, and all threads get a chance to take a step.
157        let mut threads_iter = thread_manager
158            .all_threads()
159            .skip(thread_manager.active_thread().index() + 1)
160            .chain(thread_manager.all_threads().take(thread_manager.active_thread().index() + 1))
161            .filter(|(_id, thread)| thread.is_enabled());
162        // Pick a new thread, and switch to it.
163        let new_thread = if thread_manager.fixed_scheduling() {
164            let next = threads_iter.next();
165            drop(threads_iter);
166            next
167        } else {
168            threads_iter.choose(rng)
169        };
170
171        if let Some((id, _thread)) = new_thread {
172            if thread_manager.active_thread() != id {
173                thread_manager.set_active_thread(id);
174            }
175        }
176        // This completes the `yield`, if any was requested.
177        thread_manager.yield_active_thread = false;
178
179        if thread_manager.active_thread_ref().is_enabled() {
180            return interp_ok(SchedulingAction::ExecuteStep);
181        }
182
183        // We have not found a thread to execute.
184        if ecx.machine.threads.all_threads().all(|(_id, thread)| thread.is_terminated()) {
185            unreachable!("all threads terminated without the main thread terminating?!");
186        } else if let Some(sleep_time) = potential_sleep_time {
187            // All threads are currently blocked, but we have unexecuted
188            // timeout_callbacks, which may unblock some of the threads. Hence,
189            // sleep until the first callback.
190            interp_ok(SchedulingAction::SleepAndWaitForIo(Some(sleep_time)))
191        } else if ecx.any_thread_blocked_on_host() {
192            // At least one thread doesn't have a timeout set, and is blocked on host I/O or is waiting on an
193            // epoll instance which contains a host source interest. Hence, we sleep indefinitely in the
194            // hope that eventually an I/O event happens.
195            interp_ok(SchedulingAction::SleepAndWaitForIo(None))
196        } else {
197            throw_machine_stop!(TerminationInfo::GlobalDeadlock);
198        }
199    }
200}
201
202impl<'tcx> EvalContextExt<'tcx> for crate::MiriInterpCx<'tcx> {}
203pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
204    /// Public because this is used by Priroda.
205    fn step_current_thread(&mut self) -> InterpResult<'tcx> {
206        let ecx = self.eval_context_mut();
207
208        if !ecx.step()? {
209            // The stack is empty. See if this thread can do something else.
210            match ecx.run_on_stack_empty()? {
211                Poll::Pending => {} // keep going
212                Poll::Ready(()) => {
213                    ecx.terminate_active_thread(/* for_exit */ false)?;
214                }
215            }
216        }
217
218        interp_ok(())
219    }
220
221    /// Run the core interpreter loop. Returns only when an interrupt occurs (an error or program
222    /// termination).
223    fn run_threads(&mut self) -> InterpResult<'tcx, !> {
224        let ecx = self.eval_context_mut();
225        loop {
226            if CTRL_C_RECEIVED.load(Ordering::Relaxed) {
227                ecx.machine.handle_abnormal_termination();
228                throw_machine_stop!(TerminationInfo::Interrupted);
229            }
230            match ecx.schedule()? {
231                SchedulingAction::ExecuteStep => {
232                    ecx.step_current_thread()?;
233                }
234                SchedulingAction::SleepAndWaitForIo(duration) => {
235                    if ecx.machine.communicate() {
236                        // When we're running with isolation disabled, instead of
237                        // strictly sleeping the duration we allow waking up
238                        // early for I/O events from the OS.
239
240                        ecx.poll_and_unblock(duration)?;
241                    } else {
242                        let duration = duration.expect(
243                            "Infinite sleep should not be triggered when isolation is enabled",
244                        );
245                        ecx.machine.monotonic_clock.sleep(duration);
246                    }
247                }
248            }
249        }
250    }
251}