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}