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}