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