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.
21///
22/// # Examples
23///
24/// ```
25/// use strided_exec::Pool;
26///
27/// let pool = Pool::builder().threads(2).thread_name(|i| format!("w{i}")).build()?;
28/// assert!(pool.width().get() <= 2);
29/// # Ok::<(), strided_exec::Error>(())
30/// ```
31pub struct Pool {
32    #[cfg(feature = "parallel")]
33    pub(crate) inner: rayon::ThreadPool,
34    pub(crate) width: NonZeroUsize,
35}
36
37impl Pool {
38    /// Starts configuring a pool.
39    ///
40    /// # Examples
41    ///
42    /// ```
43    /// let pool = strided_exec::Pool::builder().threads(1).build()?;
44    /// assert_eq!(pool.width().get(), 1);
45    /// # Ok::<(), strided_exec::Error>(())
46    /// ```
47    pub fn builder() -> PoolBuilder {
48        PoolBuilder::new()
49    }
50
51    /// Number of worker threads, the largest budget a token can have.
52    ///
53    /// Without the `parallel` feature this is always 1.
54    ///
55    /// # Examples
56    ///
57    /// ```
58    /// let pool = strided_exec::Pool::builder().threads(1).build()?;
59    /// assert_eq!(pool.width().get(), 1);
60    /// # Ok::<(), strided_exec::Error>(())
61    /// ```
62    pub fn width(&self) -> NonZeroUsize {
63        self.width
64    }
65}
66
67impl fmt::Debug for Pool {
68    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
69        f.debug_struct("Pool").field("width", &self.width).finish()
70    }
71}
72
73/// Configuration of a [`Pool`].
74///
75/// # Examples
76///
77/// ```
78/// use strided_exec::Pool;
79///
80/// let pool = Pool::builder()
81///     .threads(2)
82///     .stack_size(8 << 20)
83///     .on_start(|_worker| { /* e.g. pin the thread */ })
84///     .build()?;
85/// assert!(pool.width().get() >= 1);
86/// # Ok::<(), strided_exec::Error>(())
87/// ```
88#[cfg_attr(not(feature = "parallel"), allow(dead_code))]
89pub struct PoolBuilder {
90    threads: Option<usize>,
91    stack_size: usize,
92    thread_name: Option<Box<dyn Fn(usize) -> String>>,
93    on_start: Option<Box<dyn Fn(usize) + Send + Sync>>,
94}
95
96impl PoolBuilder {
97    fn new() -> Self {
98        PoolBuilder {
99            threads: None,
100            stack_size: DEFAULT_STACK_BYTES,
101            thread_name: None,
102            on_start: None,
103        }
104    }
105
106    /// Number of worker threads. Defaults to the available parallelism.
107    ///
108    /// Zero is rejected by [`build`](Self::build); the pool never falls back to
109    /// an environment-dependent default.
110    ///
111    /// # Examples
112    ///
113    /// ```
114    /// use strided_exec::{Error, Pool};
115    ///
116    /// assert_eq!(Pool::builder().threads(0).build().unwrap_err(), Error::ZeroThreads);
117    /// ```
118    #[must_use]
119    pub fn threads(mut self, n: usize) -> Self {
120        self.threads = Some(n);
121        self
122    }
123
124    /// Names the workers; the closure receives the worker index.
125    ///
126    /// # Examples
127    ///
128    /// ```
129    /// let pool = strided_exec::Pool::builder()
130    ///     .threads(1)
131    ///     .thread_name(|i| format!("my-pool-{i}"))
132    ///     .build()?;
133    /// # let _ = pool;
134    /// # Ok::<(), strided_exec::Error>(())
135    /// ```
136    #[must_use]
137    pub fn thread_name(mut self, f: impl Fn(usize) -> String + 'static) -> Self {
138        self.thread_name = Some(Box::new(f));
139        self
140    }
141
142    /// Worker stack size in bytes. Defaults to 16 MiB, sized for recursive LU.
143    ///
144    /// # Examples
145    ///
146    /// ```
147    /// let pool = strided_exec::Pool::builder().threads(1).stack_size(4 << 20).build()?;
148    /// # let _ = pool;
149    /// # Ok::<(), strided_exec::Error>(())
150    /// ```
151    #[must_use]
152    pub fn stack_size(mut self, bytes: usize) -> Self {
153        self.stack_size = bytes;
154        self
155    }
156
157    /// Hook run on each worker when it starts, with the worker index.
158    ///
159    /// Typical use is pinning the worker to a core. The hook is not called
160    /// without the `parallel` feature, which starts no workers.
161    ///
162    /// # Examples
163    ///
164    /// ```
165    /// let pool = strided_exec::Pool::builder().threads(1).on_start(|_| {}).build()?;
166    /// # let _ = pool;
167    /// # Ok::<(), strided_exec::Error>(())
168    /// ```
169    #[must_use]
170    pub fn on_start(mut self, f: impl Fn(usize) + Send + Sync + 'static) -> Self {
171        self.on_start = Some(Box::new(f));
172        self
173    }
174
175    /// Starts the workers.
176    ///
177    /// Without the `parallel` feature no thread is started and the pool has
178    /// width 1.
179    ///
180    /// # Errors
181    ///
182    /// [`Error::ZeroThreads`] when `threads(0)` was requested, and
183    /// [`Error::PoolBuild`] when the workers cannot be started.
184    ///
185    /// # Examples
186    ///
187    /// ```
188    /// let pool = strided_exec::Pool::builder().threads(2).build()?;
189    /// assert!(pool.width().get() <= 2);
190    /// # Ok::<(), strided_exec::Error>(())
191    /// ```
192    pub fn build(self) -> Result<Pool, Error> {
193        if self.threads == Some(0) {
194            return Err(Error::ZeroThreads);
195        }
196        self.build_checked()
197    }
198
199    #[cfg(feature = "parallel")]
200    fn build_checked(self) -> Result<Pool, Error> {
201        let n = self
202            .threads
203            .unwrap_or_else(|| std::thread::available_parallelism().map_or(1, NonZeroUsize::get));
204        let mut builder = rayon::ThreadPoolBuilder::new()
205            .num_threads(n)
206            .stack_size(self.stack_size);
207        if let Some(name) = self.thread_name {
208            builder = builder.thread_name(name);
209        }
210        if let Some(hook) = self.on_start {
211            builder = builder.start_handler(hook);
212        }
213        let inner = builder
214            .build()
215            .map_err(|e| Error::PoolBuild(e.to_string()))?;
216        let width = NonZeroUsize::new(inner.current_num_threads()).unwrap_or(NonZeroUsize::MIN);
217        Ok(Pool { inner, width })
218    }
219
220    #[cfg(not(feature = "parallel"))]
221    fn build_checked(self) -> Result<Pool, Error> {
222        Ok(Pool {
223            width: NonZeroUsize::MIN,
224        })
225    }
226}
227
228impl fmt::Debug for PoolBuilder {
229    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
230        f.debug_struct("PoolBuilder")
231            .field("threads", &self.threads)
232            .field("stack_size", &self.stack_size)
233            .finish_non_exhaustive()
234    }
235}