commonware_parallel/rayon/thread_pool.rs
1//! Contains support for user-managed thread pools, represented by the
2//! the [`ThreadPool`] type (see that struct for details).
3
4#[cfg(test)]
5use super::registry::WorkerThread;
6use super::{
7 ThreadPoolBuildError, ThreadPoolBuilder,
8 broadcast::{self, BroadcastContext},
9 latch::OnceLatch,
10 registry::Registry,
11 spawn,
12};
13use std::{
14 fmt,
15 future::Future,
16 pin::Pin,
17 sync::Arc,
18 task::{Context, Poll, Wake, Waker},
19};
20
21#[cfg(test)]
22mod future_test;
23#[cfg(test)]
24mod test;
25
26/// Represents a user-created [thread pool].
27///
28/// Use a [`ThreadPoolBuilder`] to specify the number and/or names of threads
29/// in the pool. After calling [`ThreadPoolBuilder::build()`], you can then
30/// execute functions explicitly within this [`ThreadPool`] using
31/// [`ThreadPool::install()`].
32///
33/// [`install()`][`ThreadPool::install()`] executes a closure in one of the `ThreadPool`'s
34/// threads. In addition, any other scheduler operations called inside of `install()` will also
35/// execute in the context of the `ThreadPool`.
36///
37/// When the `ThreadPool` is dropped, that's a signal for the threads it manages to terminate,
38/// they will complete executing any remaining work that you have spawned, and automatically
39/// terminate.
40///
41///
42/// [thread pool]: https://en.wikipedia.org/wiki/Thread_pool
43/// [`ThreadPoolBuilder::build()`]: ThreadPoolBuilder::build()
44/// [`ThreadPool::install()`]: Self::install()
45pub struct ThreadPool {
46 registry: Arc<Registry>,
47}
48
49struct WorkerWake {
50 latch: OnceLatch,
51 registry: Arc<Registry>,
52 index: usize,
53}
54
55impl Wake for WorkerWake {
56 fn wake(self: Arc<Self>) {
57 self.wake_by_ref();
58 }
59
60 fn wake_by_ref(self: &Arc<Self>) {
61 // SAFETY: this Arc keeps the latch and registry alive through the notification.
62 // The worker that created this waker is the only thread that waits on its latch.
63 unsafe { OnceLatch::set_and_tickle_one(&self.latch, &self.registry, self.index) };
64 }
65}
66
67impl ThreadPool {
68 pub(super) fn build(builder: ThreadPoolBuilder) -> Result<Self, ThreadPoolBuildError> {
69 let registry = Registry::new(builder)?;
70 Ok(Self { registry })
71 }
72
73 /// Executes `op` within the thread pool. Any attempts to use
74 /// `join` will then operate within that thread pool.
75 ///
76 /// # Warning: thread-local data
77 ///
78 /// Because `op` is executing within the thread pool,
79 /// thread-local data from the current thread will not be
80 /// accessible.
81 ///
82 /// # Warning: execution order
83 ///
84 /// If the current thread is part of a different thread pool, it will try to
85 /// keep busy while the `op` completes in its target pool, similar to
86 /// calling [`ThreadPool::yield_now()`] in a loop. Therefore, it may
87 /// potentially schedule other tasks to run on the current thread in the
88 /// meantime.
89 ///
90 /// # Panics
91 ///
92 /// If `op` should panic, that panic will be propagated.
93 pub fn install<OP, R>(&self, op: OP) -> R
94 where
95 OP: FnOnce() -> R + Send,
96 R: Send,
97 {
98 self.registry.in_worker(|_, _| op())
99 }
100
101 /// Executes `op` within every thread in the thread pool. Any attempts to use
102 /// `join` will then operate within that thread pool.
103 ///
104 /// Broadcasts are executed on each thread after they have exhausted their
105 /// local work queue, before they attempt work-stealing from other threads.
106 /// The goal of that strategy is to run everywhere in a timely manner
107 /// *without* being too disruptive to current work.
108 ///
109 /// # Warning: thread-local data
110 ///
111 /// Because `op` is executing within the thread pool,
112 /// thread-local data from the current thread will not be
113 /// accessible.
114 ///
115 /// # Panics
116 ///
117 /// If `op` should panic on one or more threads, exactly one panic
118 /// will be propagated, only after all threads have completed
119 /// (or panicked) their own `op`.
120 pub fn broadcast<OP, R>(&self, op: OP) -> Vec<R>
121 where
122 OP: Fn(BroadcastContext<'_>) -> R + Sync,
123 R: Send,
124 {
125 // SAFETY: `self` holds a reference that keeps the registry from terminating.
126 unsafe { broadcast::broadcast_in(op, &self.registry) }
127 }
128
129 /// Returns the number of threads in the thread pool.
130 #[inline]
131 pub fn current_num_threads(&self) -> usize {
132 self.registry.num_threads()
133 }
134
135 /// If called from a worker thread in this thread pool,
136 /// returns the index of that thread; if not called from a worker
137 /// thread, or called from a worker that belongs to a
138 /// different thread pool, returns `None`.
139 ///
140 /// The index for a given thread will not change over the thread's
141 /// lifetime. However, multiple threads may share the same index if
142 /// they are in distinct thread pools.
143 #[inline]
144 pub fn current_thread_index(&self) -> Option<usize> {
145 let curr = self.registry.current_thread()?;
146 Some(curr.index())
147 }
148
149 /// Polls `future` to completion while the calling worker services this pool's jobs.
150 /// Returns `None` without polling if the caller does not belong to this pool.
151 ///
152 /// Jobs run on the caller's stack, so blocking waits within them must allow nesting.
153 pub(crate) fn block_on_worker<F: Future + ?Sized>(
154 &self,
155 mut future: Pin<&mut F>,
156 ) -> Option<F::Output> {
157 let worker = self.registry.current_thread()?;
158 loop {
159 // A future may retain a waker after it is polled again or completes.
160 // Each poll owns its latch so those wakers remain valid independently.
161 let wake = Arc::new(WorkerWake {
162 latch: OnceLatch::new(),
163 registry: Arc::clone(worker.registry()),
164 index: worker.index(),
165 });
166 let waker = Waker::from(Arc::clone(&wake));
167 match future.as_mut().poll(&mut Context::from_waker(&waker)) {
168 Poll::Ready(result) => return Some(result),
169 Poll::Pending => {
170 // SAFETY: this is the calling worker, and the local Arc keeps its
171 // latch alive until the scheduler returns from the wait.
172 unsafe { worker.wait_until(&wake.latch) };
173 }
174 }
175 }
176 }
177
178 /// Spawns an asynchronous task in this thread pool. This task may
179 /// outlast the current stack frame, so it cannot capture any references
180 /// onto the stack (you will likely need a `move` closure).
181 ///
182 /// # Panics
183 ///
184 /// If the task panics, the process aborts, since the panic has nowhere to propagate.
185 pub fn spawn<OP>(&self, op: OP)
186 where
187 OP: FnOnce() + Send + 'static,
188 {
189 // SAFETY: `self` holds a reference that keeps the registry from terminating.
190 unsafe { spawn::spawn_in(op, &self.registry) }
191 }
192
193 /// Spawns an asynchronous task on every thread in this thread pool. This task
194 /// may outlast the current stack frame, so it cannot capture any references
195 /// onto the stack (you will likely need a `move` closure).
196 ///
197 /// # Panics
198 ///
199 /// If the task panics on any thread, the process aborts, since the panic has nowhere to
200 /// propagate.
201 pub fn spawn_broadcast<OP>(&self, op: OP)
202 where
203 OP: Fn(BroadcastContext<'_>) + Send + Sync + 'static,
204 {
205 // SAFETY: `self` holds a reference that keeps the registry from terminating.
206 unsafe { broadcast::spawn_broadcast_in(op, &self.registry) }
207 }
208
209 /// Cooperatively yields execution to the scheduler.
210 ///
211 /// Looks for and executes one unit of work if the current thread is part of this pool.
212 ///
213 /// Returns `Some(Yield::Executed)` if anything was executed, `Some(Yield::Idle)` if
214 /// nothing was available, or `None` if the current thread is not part of this pool.
215 pub fn yield_now(&self) -> Option<Yield> {
216 let curr = self.registry.current_thread()?;
217 Some(curr.yield_now())
218 }
219}
220
221impl Drop for ThreadPool {
222 fn drop(&mut self) {
223 self.registry.terminate();
224 }
225}
226
227impl fmt::Debug for ThreadPool {
228 fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
229 fmt.debug_struct("ThreadPool")
230 .field("num_threads", &self.current_num_threads())
231 .field("id", &self.registry.id())
232 .finish()
233 }
234}
235
236/// If called from a worker thread, returns the index of that
237/// thread within its current pool; if not called from a worker thread,
238/// returns `None`.
239///
240/// The index for a given thread will not change over the thread's
241/// lifetime. However, multiple threads may share the same index if
242/// they are in distinct thread pools.
243///
244/// See also: [the `ThreadPool::current_thread_index()` method][m].
245///
246/// [m]: ThreadPool::current_thread_index()
247#[cfg(test)]
248#[inline]
249pub(crate) fn current_thread_index() -> Option<usize> {
250 // SAFETY: a non-null pointer is this thread's worker state, which outlives the call.
251 let curr = unsafe { WorkerThread::current().as_ref()? };
252 Some(curr.index())
253}
254
255/// Cooperatively yields execution to the scheduler.
256///
257/// If the current thread is part of a thread pool, this looks for a
258/// single unit of pending work in the pool, then executes it. Completion of
259/// that work might include nested work or further work stealing.
260///
261/// This is similar to [`std::thread::yield_now()`], but does not literally make
262/// that call. If you are implementing a polling loop, you may want to also
263/// yield to the OS scheduler yourself if no work was found.
264///
265/// Returns `Some(Yield::Executed)` if anything was executed, `Some(Yield::Idle)` if
266/// nothing was available, or `None` if this thread is not part of any pool at all.
267#[cfg(test)]
268pub(crate) fn yield_now() -> Option<Yield> {
269 // SAFETY: a non-null pointer is this thread's worker state, which outlives the call.
270 let thread = unsafe { WorkerThread::current().as_ref()? };
271 Some(thread.yield_now())
272}
273
274/// Result of a worker's cooperative yield to its thread pool.
275#[derive(Clone, Copy, Debug, PartialEq, Eq)]
276pub enum Yield {
277 /// Work was found and executed.
278 Executed,
279 /// No available work was found.
280 Idle,
281}