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}