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}