strided_exec/pool.rs
1//! The pool type and its builder.
2
3use crate::error::Error;
4use core::fmt;
5use core::num::NonZeroUsize;
6
7/// Default worker stack size: 16 MiB.
8///
9/// Sized for the recursive LU and divide-and-conquer providers that run on pool
10/// workers; the same value tenferro-rs used for its own pool preset.
11const DEFAULT_STACK_BYTES: usize = 16 << 20;
12
13/// A fixed set of worker threads that [`Exec`](crate::Exec) tokens place work on.
14///
15/// `Pool` is the only way to make a pool. It wraps the scheduler privately: no
16/// scheduler type appears in any signature, there is no constructor from an
17/// external pool and no accessor returning one. The pool's owner keeps the
18/// `Pool` and mints tokens from it with [`Exec::new`](crate::Exec::new).
19///
20/// Dropping a `Pool` shuts its workers down and joins them: when `drop`
21/// returns, every worker thread has exited, so the thread-local destructors of
22/// the workers (including state set up by an [`on_start`](PoolBuilder::on_start)
23/// hook) have already run. Without the `parallel` feature there are no workers
24/// and nothing is joined.
25///
26/// A pool dropped from one of its own workers cannot wait for itself: that drop
27/// signals shutdown and returns without joining, so the workers exit shortly
28/// after instead of before the return. Drop the pool from a plain thread when
29/// the guarantee matters.
30///
31/// # Examples
32///
33/// ```
34/// use strided_exec::Pool;
35///
36/// let pool = Pool::builder().threads(2).thread_name(|i| format!("w{i}")).build()?;
37/// assert!(pool.width().get() <= 2);
38/// # Ok::<(), strided_exec::Error>(())
39/// ```
40pub struct Pool {
41 // Field order matters: `inner` is dropped (shutdown signalled) before
42 // `joiner` waits for the threads.
43 #[cfg(feature = "parallel")]
44 pub(crate) inner: rayon::ThreadPool,
45 #[cfg(feature = "parallel")]
46 _joiner: Joiner,
47 pub(crate) width: NonZeroUsize,
48}
49
50impl Pool {
51 /// Starts configuring a pool.
52 ///
53 /// # Examples
54 ///
55 /// ```
56 /// let pool = strided_exec::Pool::builder().threads(1).build()?;
57 /// assert_eq!(pool.width().get(), 1);
58 /// # Ok::<(), strided_exec::Error>(())
59 /// ```
60 pub fn builder() -> PoolBuilder {
61 PoolBuilder::new()
62 }
63
64 /// Number of worker threads, the largest budget a token can have.
65 ///
66 /// Without the `parallel` feature this is always 1.
67 ///
68 /// # Examples
69 ///
70 /// ```
71 /// let pool = strided_exec::Pool::builder().threads(1).build()?;
72 /// assert_eq!(pool.width().get(), 1);
73 /// # Ok::<(), strided_exec::Error>(())
74 /// ```
75 pub fn width(&self) -> NonZeroUsize {
76 self.width
77 }
78}
79
80/// The worker threads of a pool, joined when it is dropped.
81#[cfg(feature = "parallel")]
82struct Joiner {
83 handles: std::sync::Arc<std::sync::Mutex<Vec<std::thread::JoinHandle<()>>>>,
84}
85
86#[cfg(feature = "parallel")]
87impl Drop for Joiner {
88 fn drop(&mut self) {
89 let handles = match self.handles.lock() {
90 Ok(mut guard) => std::mem::take(&mut *guard),
91 Err(poisoned) => std::mem::take(&mut *poisoned.into_inner()),
92 };
93 let me = std::thread::current().id();
94 // A worker of this pool cannot join itself: skip joining entirely.
95 if handles.iter().any(|h| h.thread().id() == me) {
96 return;
97 }
98 for handle in handles {
99 // A worker that panicked has still exited; nothing to report.
100 let _ = handle.join();
101 }
102 }
103}
104
105impl fmt::Debug for Pool {
106 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
107 f.debug_struct("Pool").field("width", &self.width).finish()
108 }
109}
110
111/// Configuration of a [`Pool`].
112///
113/// # Examples
114///
115/// ```
116/// use strided_exec::Pool;
117///
118/// let pool = Pool::builder()
119/// .threads(2)
120/// .stack_size(8 << 20)
121/// .on_start(|_worker| { /* e.g. pin the thread */ })
122/// .build()?;
123/// assert!(pool.width().get() >= 1);
124/// # Ok::<(), strided_exec::Error>(())
125/// ```
126#[cfg_attr(not(feature = "parallel"), allow(dead_code))]
127pub struct PoolBuilder {
128 threads: Option<usize>,
129 stack_size: usize,
130 thread_name: Option<Box<dyn Fn(usize) -> String>>,
131 on_start: Option<Box<dyn Fn(usize) + Send + Sync>>,
132}
133
134impl PoolBuilder {
135 fn new() -> Self {
136 PoolBuilder {
137 threads: None,
138 stack_size: DEFAULT_STACK_BYTES,
139 thread_name: None,
140 on_start: None,
141 }
142 }
143
144 /// Number of worker threads. Defaults to the available parallelism.
145 ///
146 /// Zero is rejected by [`build`](Self::build); the pool never falls back to
147 /// an environment-dependent default.
148 ///
149 /// # Examples
150 ///
151 /// ```
152 /// use strided_exec::{Error, Pool};
153 ///
154 /// assert_eq!(Pool::builder().threads(0).build().unwrap_err(), Error::ZeroThreads);
155 /// ```
156 #[must_use]
157 pub fn threads(mut self, n: usize) -> Self {
158 self.threads = Some(n);
159 self
160 }
161
162 /// Names the workers; the closure receives the worker index.
163 ///
164 /// # Examples
165 ///
166 /// ```
167 /// let pool = strided_exec::Pool::builder()
168 /// .threads(1)
169 /// .thread_name(|i| format!("my-pool-{i}"))
170 /// .build()?;
171 /// # let _ = pool;
172 /// # Ok::<(), strided_exec::Error>(())
173 /// ```
174 #[must_use]
175 pub fn thread_name(mut self, f: impl Fn(usize) -> String + 'static) -> Self {
176 self.thread_name = Some(Box::new(f));
177 self
178 }
179
180 /// Worker stack size in bytes. Defaults to 16 MiB, sized for recursive LU.
181 ///
182 /// # Examples
183 ///
184 /// ```
185 /// let pool = strided_exec::Pool::builder().threads(1).stack_size(4 << 20).build()?;
186 /// # let _ = pool;
187 /// # Ok::<(), strided_exec::Error>(())
188 /// ```
189 #[must_use]
190 pub fn stack_size(mut self, bytes: usize) -> Self {
191 self.stack_size = bytes;
192 self
193 }
194
195 /// Hook run on each worker when it starts, with the worker index.
196 ///
197 /// Typical use is pinning the worker to a core. The hook is not called
198 /// without the `parallel` feature, which starts no workers.
199 ///
200 /// # Examples
201 ///
202 /// ```
203 /// let pool = strided_exec::Pool::builder().threads(1).on_start(|_| {}).build()?;
204 /// # let _ = pool;
205 /// # Ok::<(), strided_exec::Error>(())
206 /// ```
207 #[must_use]
208 pub fn on_start(mut self, f: impl Fn(usize) + Send + Sync + 'static) -> Self {
209 self.on_start = Some(Box::new(f));
210 self
211 }
212
213 /// Starts the workers.
214 ///
215 /// Without the `parallel` feature no thread is started and the pool has
216 /// width 1.
217 ///
218 /// # Errors
219 ///
220 /// [`Error::ZeroThreads`] when `threads(0)` was requested, and
221 /// [`Error::PoolBuild`] when the workers cannot be started.
222 ///
223 /// # Examples
224 ///
225 /// ```
226 /// let pool = strided_exec::Pool::builder().threads(2).build()?;
227 /// assert!(pool.width().get() <= 2);
228 /// # Ok::<(), strided_exec::Error>(())
229 /// ```
230 pub fn build(self) -> Result<Pool, Error> {
231 if self.threads == Some(0) {
232 return Err(Error::ZeroThreads);
233 }
234 self.build_checked()
235 }
236
237 #[cfg(feature = "parallel")]
238 fn build_checked(self) -> Result<Pool, Error> {
239 let n = self
240 .threads
241 .unwrap_or_else(|| std::thread::available_parallelism().map_or(1, NonZeroUsize::get));
242 let handles = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
243 let sink = std::sync::Arc::clone(&handles);
244 let mut builder = rayon::ThreadPoolBuilder::new()
245 .num_threads(n)
246 .stack_size(self.stack_size)
247 .spawn_handler(move |thread| {
248 let mut b = std::thread::Builder::new();
249 if let Some(name) = thread.name() {
250 b = b.name(name.to_owned());
251 }
252 if let Some(size) = thread.stack_size() {
253 b = b.stack_size(size);
254 }
255 let handle = b.spawn(move || thread.run())?;
256 match sink.lock() {
257 Ok(mut guard) => guard.push(handle),
258 Err(poisoned) => poisoned.into_inner().push(handle),
259 }
260 Ok(())
261 });
262 if let Some(name) = self.thread_name {
263 builder = builder.thread_name(name);
264 }
265 if let Some(hook) = self.on_start {
266 builder = builder.start_handler(hook);
267 }
268 // On failure `builder` drops its handler, but threads already started
269 // are signalled by rayon and detached; join them here for determinism.
270 let built = builder.build();
271 let inner = match built {
272 Ok(inner) => inner,
273 Err(e) => {
274 drop(Joiner { handles });
275 return Err(Error::PoolBuild(e.to_string()));
276 }
277 };
278 let width = NonZeroUsize::new(inner.current_num_threads()).unwrap_or(NonZeroUsize::MIN);
279 Ok(Pool {
280 inner,
281 _joiner: Joiner { handles },
282 width,
283 })
284 }
285
286 #[cfg(not(feature = "parallel"))]
287 fn build_checked(self) -> Result<Pool, Error> {
288 Ok(Pool {
289 width: NonZeroUsize::MIN,
290 })
291 }
292}
293
294impl fmt::Debug for PoolBuilder {
295 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
296 f.debug_struct("PoolBuilder")
297 .field("threads", &self.threads)
298 .field("stack_size", &self.stack_size)
299 .finish_non_exhaustive()
300 }
301}
302
303#[cfg(all(test, feature = "parallel"))]
304mod tests {
305 use super::*;
306 use std::sync::{mpsc, Arc};
307 use std::time::Duration;
308
309 /// The last reference is dropped on a worker of the pool itself: the drop
310 /// must not wait for its own thread.
311 #[test]
312 fn drop_from_a_worker_of_the_pool_does_not_hang() {
313 let pool = Arc::new(Pool::builder().threads(2).build().unwrap());
314 let last = Arc::clone(&pool);
315 let (gate_tx, gate_rx) = mpsc::channel::<()>();
316 let (done_tx, done_rx) = mpsc::channel::<()>();
317 pool.inner.spawn(move || {
318 gate_rx.recv().unwrap();
319 drop(last); // the last reference: runs `Pool::drop` on this worker
320 done_tx.send(()).unwrap();
321 });
322 drop(pool);
323 gate_tx.send(()).unwrap();
324 done_rx
325 .recv_timeout(Duration::from_secs(10))
326 .expect("drop on a worker hung");
327 }
328}