Skip to main content

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}