Skip to main content

moirai_executor/schedule/runtime/
worker.rs

1//! Worker loop and associated free functions for the thread scheduler runtime.
2
3mod indexed;
4mod wait;
5
6use std::{
7    sync::{Mutex, MutexGuard},
8    thread::{self, JoinHandle},
9};
10
11use super::super::job::ScheduledJob;
12use super::super::queue::WorkerQueueOwner;
13
14use super::idle_hooks::run_idle_hooks;
15use super::scheduler::placement::WorkerPin;
16use super::types::{ContendedWakePolicy, SchedulerInner, WorkerState, set_current_worker_id};
17pub(super) use indexed::{
18    indexed_chunk_bounds, indexed_chunk_count, inline_map_reduce, map_reduce_range,
19};
20use wait::{should_stop, spin_for_work, wait_for_work};
21
22#[cfg(feature = "scheduler-diagnostics")]
23use super::types::BoundedContendedWake;
24
25pub(super) const WORKER_IDLE_SPIN_ATTEMPTS: usize = 256;
26pub(super) const JOIN_FAST_SPIN_ATTEMPTS: usize = WORKER_IDLE_SPIN_ATTEMPTS;
27
28/// Join every worker except the thread currently executing shutdown.
29///
30/// A runtime may lose its final external owner inside one of its own jobs. The
31/// current worker cannot join itself; dropping that handle detaches it, while
32/// its local scheduler `Arc` keeps runtime state alive until the job and worker
33/// loop return.
34pub(super) fn join_other_threads(handles: &mut Vec<JoinHandle<()>>) {
35    let current = thread::current().id();
36    while let Some(handle) = handles.pop() {
37        if handle.thread().id() != current {
38            let _ = handle.join();
39        }
40    }
41}
42
43pub(super) fn worker_loop<const BLOCKING_QUEUE_CAPACITY: usize, const SPIN_LIMIT: usize>(
44    inner: std::sync::Arc<SchedulerInner<BLOCKING_QUEUE_CAPACITY>>,
45    worker_id: usize,
46    mut owner: WorkerQueueOwner,
47    pin: Option<WorkerPin>,
48) {
49    set_current_worker_id(Some(worker_id));
50    let _ = inner.workers[worker_id].thread.set(thread::current());
51
52    // A pinned worker publishes its binding outcome before doing any work and
53    // never runs unbound: construction is waiting on this cell.
54    if let Some(pin) = pin {
55        let outcome = pin.bind();
56        let refused = outcome.is_err();
57        let _ = inner.workers[worker_id].placement.set(outcome);
58        if refused {
59            return;
60        }
61    }
62
63    loop {
64        if let Some(job) = next_job(&inner, worker_id, &mut owner) {
65            execute_job(&inner, worker_id, job);
66            continue;
67        }
68
69        if should_stop(&inner) {
70            break;
71        }
72
73        if spin_for_work::<BLOCKING_QUEUE_CAPACITY, SPIN_LIMIT>(&inner, worker_id) {
74            continue;
75        }
76
77        // Run defragmentation sweeps only right before blocking in wait_for_work
78        // to avoid latency overheads during active work stealing and spinning.
79        run_idle_memory_maintenance();
80
81        owner.shrink_drained_planes();
82
83        // Consumer-registered idle hooks run at the same quiescent point: the
84        // worker found no work and is about to block, so any thread-local
85        // reclamation a hook performs costs nothing on the active path.
86        run_idle_hooks();
87
88        wait_for_work(&inner, worker_id);
89    }
90}
91
92#[cfg(feature = "mnemosyne")]
93melinoe::thread_cached! {
94    mod last_maintenance_time: std::time::Instant;
95}
96
97#[inline]
98fn run_idle_memory_maintenance() {
99    #[cfg(feature = "mnemosyne")]
100    {
101        use mnemosyne::{LocalAllocatorSelector, MemoryBackendWrapper};
102        if <MemoryBackendWrapper as LocalAllocatorSelector<MemoryBackendWrapper>>::get_allocator_ptr_raw().is_null() {
103            return;
104        }
105
106        let now = std::time::Instant::now();
107        let should_run = if let Some(last) = last_maintenance_time::get() {
108            if now.duration_since(last) >= std::time::Duration::from_millis(500) {
109                last_maintenance_time::set(now);
110                true
111            } else {
112                false
113            }
114        } else {
115            last_maintenance_time::set(now);
116            true
117        };
118
119        if should_run {
120            let _ =
121                <MemoryBackendWrapper as LocalAllocatorSelector<MemoryBackendWrapper>>::with_allocator(
122                    // SAFETY: the selector hands this closure the allocator
123                    // instance registered for this worker thread; the sweep
124                    // requires exactly that exclusive per-thread allocator
125                    // view for its duration.
126                    |alloc| unsafe {
127                        alloc.periodic_defragmentation_sweep();
128                    },
129                );
130        }
131    }
132}
133
134pub(super) fn next_job<const BLOCKING_QUEUE_CAPACITY: usize>(
135    inner: &SchedulerInner<BLOCKING_QUEUE_CAPACITY>,
136    worker_id: usize,
137    owner: &mut WorkerQueueOwner,
138) -> Option<ScheduledJob> {
139    let local = &inner.workers[worker_id];
140    local
141        .lifo_slot
142        .pop()
143        .or_else(|| owner.pop_local())
144        .or_else(|| steal_job(inner, worker_id, owner))
145}
146
147/// Obtain runnable work using only shared top-side capabilities.
148pub(super) fn next_shared_job<const BLOCKING_QUEUE_CAPACITY: usize>(
149    inner: &SchedulerInner<BLOCKING_QUEUE_CAPACITY>,
150    worker_id: usize,
151) -> Option<ScheduledJob> {
152    let local = &inner.workers[worker_id];
153    local
154        .lifo_slot
155        .pop()
156        .or_else(|| local.queues.steal_one())
157        .or_else(|| steal_shared_job(inner, worker_id))
158}
159
160fn steal_job<const BLOCKING_QUEUE_CAPACITY: usize>(
161    inner: &SchedulerInner<BLOCKING_QUEUE_CAPACITY>,
162    worker_id: usize,
163    owner: &mut WorkerQueueOwner,
164) -> Option<ScheduledJob> {
165    let worker_count = inner.workers.len();
166    let my_node = inner.worker_numa_nodes.get(worker_id).copied().flatten();
167
168    // Two-pass NUMA-aware victim selection:
169    //
170    // Pass 1 (same-NUMA-node): prefer victims on the same NUMA node as the
171    // thief.  Same-node steals access memory already in the local NUMA bank,
172    // avoiding cross-socket NUMA traffic on multi-socket systems.  Skipped
173    // when topology is unavailable (my_node == None) or only one node exists.
174    //
175    // Pass 2 (all workers): fall back to the full-ring randomised scan so
176    // coverage and worst-case load balance are preserved — same as the
177    // previous implementation.
178    //
179    // Both passes use xorshift64 randomisation (Blumofe–Leiserson) to spread
180    // the first steal attempt and prevent thundering-herd CAS contention.
181    if let Some(node) = my_node {
182        let start = next_steal_start();
183        for offset in 0..worker_count {
184            let victim_index = (start.wrapping_add(offset)) % worker_count;
185            if victim_index == worker_id {
186                continue;
187            }
188            // Only try same-node victims in pass 1.
189            if inner.worker_numa_nodes.get(victim_index).copied().flatten() != Some(node) {
190                continue;
191            }
192            let victim = &inner.workers[victim_index];
193            if let Some(job) = owner.steal_batch(&victim.queues) {
194                return Some(job);
195            }
196            if let Some(job) = victim.lifo_slot.steal() {
197                return Some(job);
198            }
199        }
200    }
201
202    // Pass 2: full ring scan from a fresh random origin, skipping self.
203    let start = next_steal_start();
204    for offset in 0..worker_count {
205        let victim_index = (start.wrapping_add(offset)) % worker_count;
206        if victim_index == worker_id {
207            continue;
208        }
209        let victim = &inner.workers[victim_index];
210        if let Some(job) = owner.steal_batch(&victim.queues) {
211            return Some(job);
212        }
213        if let Some(job) = victim.lifo_slot.steal() {
214            return Some(job);
215        }
216    }
217
218    None
219}
220
221fn steal_shared_job<const BLOCKING_QUEUE_CAPACITY: usize>(
222    inner: &SchedulerInner<BLOCKING_QUEUE_CAPACITY>,
223    worker_id: usize,
224) -> Option<ScheduledJob> {
225    let worker_count = inner.workers.len();
226    let start = next_steal_start();
227    for offset in 0..worker_count {
228        let victim_index = start.wrapping_add(offset) % worker_count;
229        if victim_index == worker_id {
230            continue;
231        }
232        let victim = &inner.workers[victim_index];
233        if let Some(job) = victim.queues.steal_one() {
234            return Some(job);
235        }
236        if let Some(job) = victim.lifo_slot.steal() {
237            return Some(job);
238        }
239    }
240    None
241}
242
243/// Thread-local xorshift64 producing a randomized starting victim index.
244///
245/// Seeded lazily from the per-thread RNG cell's own address (stable and unique
246/// per worker thread, forced non-zero), so it needs no shared atomic on the hot
247/// path -- the seed source is contention-free by construction.
248fn next_steal_start() -> usize {
249    use std::cell::Cell;
250    std::thread_local! {
251        // clippy 1.97.0 FP: already const. ATLAS-MNEMOSYNE-CI-1.
252        #[allow(clippy::missing_const_for_thread_local)]
253        static RNG: Cell<u64> = const { Cell::new(0) };
254    }
255    RNG.with(|cell| {
256        let mut x = cell.get();
257        if x == 0 {
258            x = (cell as *const Cell<u64> as u64) | 1;
259        }
260        x ^= x << 13;
261        x ^= x >> 7;
262        x ^= x << 17;
263        cell.set(x);
264        x as usize
265    })
266}
267
268pub(super) fn execute_job<const BLOCKING_QUEUE_CAPACITY: usize>(
269    inner: &SchedulerInner<BLOCKING_QUEUE_CAPACITY>,
270    worker_id: usize,
271    job: ScheduledJob,
272) {
273    execute_job_with_counters(
274        inner,
275        worker_id,
276        job,
277        &inner.pending_tasks,
278        &inner.active_workers,
279    );
280}
281
282pub(super) fn execute_blocking_job<const BLOCKING_QUEUE_CAPACITY: usize>(
283    inner: &SchedulerInner<BLOCKING_QUEUE_CAPACITY>,
284    worker_id: usize,
285    job: ScheduledJob,
286) {
287    execute_job_with_counters(
288        inner,
289        worker_id,
290        job,
291        &inner.blocking_pending_tasks,
292        &inner.blocking_active_workers,
293    );
294}
295
296fn execute_job_with_counters<const BLOCKING_QUEUE_CAPACITY: usize>(
297    inner: &SchedulerInner<BLOCKING_QUEUE_CAPACITY>,
298    worker_id: usize,
299    job: ScheduledJob,
300    pending_tasks: &std::sync::atomic::AtomicUsize,
301    active_workers: &std::sync::atomic::AtomicUsize,
302) {
303    use std::sync::atomic::Ordering;
304    active_workers.fetch_add(1, Ordering::Release);
305    pending_tasks.fetch_sub(1, Ordering::Release);
306
307    if job.execute(worker_id) {
308        inner.completed_tasks.fetch_add(1, Ordering::Relaxed);
309    } else {
310        inner.failed_tasks.fetch_add(1, Ordering::Relaxed);
311    }
312
313    // SeqCst (not AcqRel): this decrement-to-zero publishes quiescence to a
314    // parking `join()` waiter and is one half of a store-buffer (Dekker)
315    // handshake — the worker stores `active -> 0` here then `notify_quiescent`
316    // loads `join_waiters`, while `join` stores `join_waiters += 1` then
317    // `is_quiescent` loads `active`. All four accesses must share one SeqCst
318    // total order; with AcqRel the StoreLoad reordering admits the lost-wakeup
319    // outcome (joiner reads stale `active != 0` and parks while the worker reads
320    // stale `join_waiters == 0` and never signals — a hung `join()`), proven
321    // reachable by `tests/loom_join_quiescence.rs`. On x86 `lock sub`/`lock xadd`
322    // is already a full barrier, so this is free.
323    if active_workers.fetch_sub(1, Ordering::SeqCst) == 1 {
324        notify_quiescent(inner);
325    }
326}
327
328pub(super) fn wake_worker(worker: &WorkerState) {
329    if let Some(thread) = worker.thread.get() {
330        thread.unpark();
331    }
332}
333
334pub(super) fn wake_all_workers<const BLOCKING_QUEUE_CAPACITY: usize>(
335    inner: &SchedulerInner<BLOCKING_QUEUE_CAPACITY>,
336) {
337    for worker in inner.workers.iter() {
338        wake_worker(worker);
339    }
340}
341
342pub(super) trait ContendedWakable {
343    /// Worker-pool size; consumed only by the diagnostics wake-decision path.
344    #[cfg(feature = "scheduler-diagnostics")]
345    fn worker_count(&self) -> usize;
346    /// Direct single-worker wake; consumed only by the diagnostics wake-decision path.
347    #[cfg(feature = "scheduler-diagnostics")]
348    fn wake_worker(&self, worker_index: usize);
349    fn wake_contended<P>(&self, worker_index: usize, previous_pending: usize) -> usize
350    where
351        P: ContendedWakePolicy;
352}
353
354impl<const BLOCKING_QUEUE_CAPACITY: usize> ContendedWakable
355    for SchedulerInner<BLOCKING_QUEUE_CAPACITY>
356{
357    #[cfg(feature = "scheduler-diagnostics")]
358    fn worker_count(&self) -> usize {
359        self.workers.len()
360    }
361
362    #[cfg(feature = "scheduler-diagnostics")]
363    fn wake_worker(&self, worker_index: usize) {
364        wake_worker(&self.workers[worker_index]);
365    }
366
367    fn wake_contended<P>(&self, worker_index: usize, previous_pending: usize) -> usize
368    where
369        P: ContendedWakePolicy,
370    {
371        let worker_count = self.workers.len();
372        wake_worker(&self.workers[worker_index]);
373
374        if P::WAKE_LIMIT < 2 || worker_count < 2 {
375            return 1;
376        }
377
378        let peer_index = worker_index.wrapping_add(previous_pending) % worker_count;
379        wake_worker(&self.workers[peer_index]);
380        2
381    }
382}
383
384#[cold]
385#[inline(never)]
386pub(super) fn wake_contended_workers<P>(
387    inner: &impl ContendedWakable,
388    worker_index: usize,
389    previous_pending: usize,
390) -> usize
391where
392    P: ContendedWakePolicy,
393{
394    inner.wake_contended::<P>(worker_index, previous_pending)
395}
396
397#[cfg(feature = "scheduler-diagnostics")]
398#[inline]
399pub(super) fn diagnostic_publish_work_available(
400    inner: &impl ContendedWakable,
401    worker_index: usize,
402    previous_pending: usize,
403) -> usize {
404    let worker_count = inner.worker_count();
405    if previous_pending == 0 {
406        inner.wake_worker(worker_index);
407        1
408    } else if previous_pending < worker_count {
409        wake_contended_workers::<BoundedContendedWake>(inner, worker_index, previous_pending)
410    } else {
411        0
412    }
413}
414
415pub(super) fn notify_quiescent<const BLOCKING_QUEUE_CAPACITY: usize>(
416    inner: &SchedulerInner<BLOCKING_QUEUE_CAPACITY>,
417) {
418    use std::sync::atomic::Ordering;
419    // SeqCst: the worker half of the quiescence Dekker handshake (see the
420    // `active_workers` decrement in `execute_job`). This load must be in the same
421    // SeqCst total order as `join`'s `join_waiters` increment, or a just-arrived
422    // waiter is missed.
423    if inner.join_waiters.load(Ordering::SeqCst) != 0 && is_quiescent(inner) {
424        let _guard = lock_mutex(&inner.wait_lock);
425        if is_quiescent(inner) {
426            inner.wait_signal.notify_all();
427        }
428    }
429}
430
431pub(super) fn is_quiescent<const BLOCKING_QUEUE_CAPACITY: usize>(
432    inner: &SchedulerInner<BLOCKING_QUEUE_CAPACITY>,
433) -> bool {
434    use std::sync::atomic::Ordering;
435    // SeqCst: the `active_workers` load is the joiner's half of the quiescence
436    // Dekker handshake (see `execute_job`) and must sit in the shared SeqCst
437    // total order; `pending_tasks` is loaded SeqCst too so the full quiescence
438    // predicate is evaluated against one consistent order. SeqCst loads are a
439    // plain load on x86 (`mov`), so this is cheap on the common target.
440    inner.pending_tasks.load(Ordering::SeqCst) == 0
441        && inner.active_workers.load(Ordering::SeqCst) == 0
442        && inner.blocking_pending_tasks.load(Ordering::SeqCst) == 0
443        && inner.blocking_active_workers.load(Ordering::SeqCst) == 0
444}
445
446pub(super) fn lock_mutex<T>(mutex: &Mutex<T>) -> MutexGuard<'_, T> {
447    mutex
448        .lock()
449        .unwrap_or_else(|poisoned| poisoned.into_inner())
450}