Skip to main content

concinnity_core/physics/fanout/
inline.rs

1// How a caller lends the simulation its threads.
2//
3// The simulation cannot own a scheduler. It is `#![no_std]` and depends on two
4// leaves, so it can neither name a thread pool nor detect that one exists. The
5// work it can split is therefore offered rather than taken: a step says what it
6// holds that is independent, and whoever called it decides whether that runs on
7// one thread or several.
8//
9// The trait is generic in the item and the body rather than object safe. A step
10// hands out a small fixed array of work units, so the fan-out is monomorphised
11// into the step that used it and nothing is boxed or dispatched dynamically.
12//
13// `scope` is the second half of that, and it is what makes the first one
14// affordable. Reaching a pool of sleeping workers from a thread that is not one
15// of them costs far more than the handing-out does; reaching it again from
16// inside costs almost nothing. So a step gathers the workers once and offers
17// all three of its stages inside that, rather than paying the entry three
18// times. A fan-out with nothing to gather leaves the default alone.
19//
20// A caller that lends nothing gets `Inline`, which runs the units in order on
21// the calling thread. That is the default `Simulation::step` uses, and it is
22// why a single-threaded host needs no capability check and no configuration.
23
24/// A caller's way of running independent work at the same time.
25///
26/// The simulation asks for one of these rather than depending on a scheduler,
27/// so the same step runs on a thread pool, on one thread, or on a host that has
28/// no threads at all.
29///
30/// # Examples
31///
32/// ```
33/// use concinnity_core::physics::{Fanout, Inline};
34///
35/// let mut work = [1u32, 2, 3];
36/// Inline.for_each(&mut work, |item| *item *= 10);
37/// assert_eq!(work, [10, 20, 30]);
38/// assert_eq!(Inline.workers(), 1);
39/// ```
40pub trait Fanout: Sync {
41    /// Units of work this fan-out can run at once. One means the work runs on
42    /// the calling thread, which is what the simulation sizes its per-worker
43    /// scratch against.
44    fn workers(&self) -> usize;
45
46    /// Run `work` with this fan-out's workers already gathered, and return
47    /// what it produced.
48    ///
49    /// Everything a step hands out happens inside one of these. An
50    /// implementation backed by a thread pool enters it here, so the
51    /// [`Fanout::for_each`] calls inside are already there; one with no pool
52    /// to enter leaves this as it is and runs `work` where it stands.
53    fn scope<R, F>(&self, work: F) -> R
54    where
55        F: FnOnce() -> R + Send,
56        R: Send,
57    {
58        work()
59    }
60
61    /// Run `body` over every item and return once all of them are done.
62    ///
63    /// Items are independent, so an implementation may visit them in any order
64    /// and on any thread. The simulation never lets the result depend on which
65    /// one did what.
66    fn for_each<T, F>(&self, items: &mut [T], body: F)
67    where
68        T: Send,
69        F: Fn(&mut T) + Send + Sync;
70}
71
72/// The fan-out for a caller that has no threads to lend: every unit runs on the
73/// calling thread, in order.
74#[derive(Debug, Clone, Copy, Default)]
75pub struct Inline;
76
77impl Fanout for Inline {
78    fn workers(&self) -> usize {
79        1
80    }
81
82    fn for_each<T, F>(&self, items: &mut [T], body: F)
83    where
84        T: Send,
85        F: Fn(&mut T) + Send + Sync,
86    {
87        for item in items {
88            body(item);
89        }
90    }
91}
92
93#[cfg(test)]
94mod tests {
95    use super::*;
96    use alloc::vec::Vec;
97
98    #[test]
99    fn inline_visits_every_item_in_order() {
100        let mut items: Vec<u32> = (0..8).collect();
101        let mut seen = Vec::new();
102        // A `Fn` cannot hold the log mutably, so the order is recovered from
103        // what the items themselves end up carrying.
104        Inline.for_each(&mut items, |item| *item += 100);
105        seen.extend(items.iter().copied());
106        assert_eq!(seen, (0..8).map(|i| i + 100).collect::<Vec<_>>());
107    }
108
109    #[test]
110    fn inline_lends_one_worker() {
111        assert_eq!(Inline.workers(), 1);
112    }
113
114    #[test]
115    fn an_empty_batch_is_a_no_op() {
116        let mut items: [u32; 0] = [];
117        Inline.for_each(&mut items, |item| *item += 1);
118        assert!(items.is_empty());
119    }
120}