Skip to main content

aoc/util/
thread.rs

1//! Utility methods to spawn a number of
2//! [scoped](https://doc.rust-lang.org/stable/std/thread/fn.scope.html)
3//! threads equal to the number of cores on the machine. Unlike normal threads, scoped threads
4//! can borrow data from their environment.
5use std::iter::repeat_with;
6use std::sync::atomic::Ordering::Relaxed;
7use std::sync::atomic::{AtomicBool, AtomicU32, AtomicUsize};
8use std::thread::*;
9
10pub struct ParIter<'a, T> {
11    id: usize,
12    items: &'a [T],
13    workers: &'a [CachePadding],
14}
15
16impl<'a, T> Iterator for ParIter<'a, T> {
17    type Item = &'a T;
18
19    fn next(&mut self) -> Option<&'a T> {
20        // First try taking from our own queue.
21        let worker = &self.workers[self.id];
22        let current = worker.increment();
23        let (start, end) = unpack(current);
24
25        // There are still items to process.
26        if start < end {
27            return Some(&self.items[start]);
28        }
29
30        // Steal from another worker, [spinlocking](https://en.wikipedia.org/wiki/Spinlock)
31        // until we acquire new items to process or there's nothing left to do.
32        loop {
33            // Find worker with the most remaining items, breaking out of the loop
34            // and returning `None` if there is no work remaining.
35            let (other, current, _) = self
36                .workers
37                .iter()
38                .filter_map(|other| {
39                    let current = other.load();
40                    let (start, end) = unpack(current);
41                    let size = end.saturating_sub(start);
42
43                    (size > 1).then_some((other, current, size))
44                })
45                .max_by_key(|&(.., size)| size)?;
46
47            // Split the work items into two roughly equal piles.
48            let (start, end) = unpack(current);
49            let middle = start.midpoint(end);
50
51            let next = pack(start, middle);
52            let stolen = pack(middle + 1, end);
53
54            // We could be preempted by another thread stealing or by the owning worker
55            // thread finishing an item, so check indices are still unmodified.
56            if other.compare_exchange(current, next) {
57                worker.store(stolen);
58                break Some(&self.items[middle]);
59            }
60        }
61    }
62}
63
64/// Intentionally force alignment to 128 bytes to make a best effort attempt to place each atomic
65/// on its own cache line. This reduces contention and improves performance for common
66/// CPU caching protocols such as [MESI](https://en.wikipedia.org/wiki/MESI_protocol).
67#[repr(align(128))]
68struct CachePadding {
69    atomic: AtomicUsize,
70}
71
72/// Convenience wrapper methods around atomic operations. Both start and end indices are packed
73/// into a single atomic so that we can use the fastest and easiest to reason about `Relaxed`
74/// ordering.
75impl CachePadding {
76    #[inline]
77    fn new(n: usize) -> Self {
78        Self { atomic: AtomicUsize::new(n) }
79    }
80
81    #[inline]
82    fn increment(&self) -> usize {
83        self.atomic.fetch_add(1, Relaxed)
84    }
85
86    #[inline]
87    fn load(&self) -> usize {
88        self.atomic.load(Relaxed)
89    }
90
91    #[inline]
92    fn store(&self, n: usize) {
93        self.atomic.store(n, Relaxed);
94    }
95
96    #[inline]
97    fn compare_exchange(&self, current: usize, new: usize) -> bool {
98        self.atomic.compare_exchange(current, new, Relaxed, Relaxed).is_ok()
99    }
100}
101
102/// Shares a monotonically increasing value between multiple threads.
103pub struct AtomicIter {
104    running: AtomicBool,
105    index: AtomicU32,
106    step: u32,
107}
108
109impl AtomicIter {
110    #[must_use]
111    pub fn new(start: u32, step: u32) -> Self {
112        Self { running: AtomicBool::new(true), index: AtomicU32::from(start), step }
113    }
114
115    #[must_use]
116    pub fn next(&self) -> Option<u32> {
117        self.running.load(Relaxed).then(|| self.index.fetch_add(self.step, Relaxed))
118    }
119
120    pub fn stop(&self) {
121        self.running.store(false, Relaxed);
122    }
123}
124
125/// Usually the number of physical cores.
126#[must_use]
127pub fn threads() -> usize {
128    available_parallelism().unwrap().get()
129}
130
131/// Spawn `n` scoped threads, where `n` is the available parallelism.
132pub fn spawn<F, R>(f: F) -> Vec<R>
133where
134    F: Fn() -> R + Copy + Send,
135    R: Send,
136{
137    scope(|scope| {
138        let handles: Vec<_> = repeat_with(|| scope.spawn(f)).take(threads()).collect();
139        handles.into_iter().flat_map(ScopedJoinHandle::join).collect()
140    })
141}
142
143/// Spawns `n` scoped threads that each receive a
144/// [work stealing](https://en.wikipedia.org/wiki/Work_stealing) iterator.
145/// Work stealing is an efficient strategy that keeps each CPU core busy when some items take longer
146/// than others to process, used by popular libraries such as [rayon](https://github.com/rayon-rs/rayon).
147/// Processing at different rates also happens on many modern CPUs with
148/// [heterogeneous performance and efficiency cores](https://en.wikipedia.org/wiki/ARM_big.LITTLE).
149#[must_use]
150pub fn spawn_parallel_iterator<F, R, T>(items: &[T], f: F) -> Vec<R>
151where
152    F: Fn(ParIter<'_, T>) -> R + Copy + Send,
153    R: Send,
154    T: Sync,
155{
156    let threads = threads();
157    let size = items.len().div_ceil(threads);
158
159    // Initially divide work as evenly as possible among the worker threads.
160    let workers: Vec<_> = (0..threads)
161        .map(|id| {
162            let start = (id * size).min(items.len());
163            let end = (start + size).min(items.len());
164            CachePadding::new(pack(start, end))
165        })
166        .collect();
167    let workers = workers.as_slice();
168
169    scope(|scope| {
170        let handles: Vec<_> =
171            (0..threads).map(|id| scope.spawn(move || f(ParIter { id, items, workers }))).collect();
172        handles.into_iter().flat_map(ScopedJoinHandle::join).collect()
173    })
174}
175
176#[inline]
177fn pack(start: usize, end: usize) -> usize {
178    (end << 32) | start
179}
180
181#[inline]
182fn unpack(both: usize) -> (usize, usize) {
183    (both & 0xffffffff, both >> 32)
184}