Skip to main content

concinnity_physics/
fanout.rs

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