Skip to main content

orx_parallel/pools/
thread_pool.rs

1use crate::parameters::{NumThreads, Params, non_zero_or_one};
2use crate::pools::scope::Scope;
3use crate::pools::tasks::TaskQueue;
4use core::num::NonZeroUsize;
5
6/// Abstraction for parallel execution environments and thread pool management.
7///
8/// `ThreadPool` defines how parallel computations are executed on a set of worker threads.
9/// Any type implementing this trait can serve as a thread pool for orx-parallel computations.
10///
11/// # Thread Count Decision
12///
13/// The actual number of threads used in a computation is determined by combining multiple
14/// configuration layers:
15///
16/// 1. **Pool Layer** (`max_num_threads()`) - The thread pool's maximum capacity
17/// 2. **Environment Layer** (`ORX_NUM_THREADS`) - Global limit from environment variable
18/// 3. **Computation Layer** (`.num_threads()` on Par) - Per-computation request
19/// 4. **Input Size** - Cannot exceed the number of input elements
20///
21/// The `max_num_threads_for_computation()` method implements this logic by returning the
22/// minimum of all these constraints.
23///
24/// # Example
25///
26/// ```ignore
27/// use orx_parallel::*;
28///
29/// // Pool setup: 8 threads requested, but env limits to 4
30/// // ORX_NUM_THREADS=4 is set
31/// let pool = Pool::once(8);  // pool.max_num_threads() == 4
32///
33/// // Computation: request 6 threads on 100-element input
34/// let result: Vec<_> = (0..100)
35///     .into_par()
36///     .map(|x| x * 2)
37///     .pool(pool)
38///     .num_threads(6)
39///     .collect();
40/// // Result: min(min(6, 100), 4) = 4 threads used
41/// ```
42///
43/// # Implementations
44///
45/// - `Pool::basic` - Persistent thread pool (default)
46/// - `Pool::once` - Lightweight virtual pool, spawns threads on-demand
47/// - `Pool::rayon` - rayon thread pools
48///
49/// See the [`thread_usage.md`](https://github.com/orxfun/orx-parallel/blob/main/docs/thread_usage.md) documentation for a complete guide.
50pub trait ThreadPool {
51    /// Scope type of the thread pool.
52    type ScopeRef<'s, 'env, 'scope>: Scope<'s, 'env, 'scope>
53    where
54        'scope: 's,
55        'env: 'scope + 's;
56
57    /// Executes the scoped computation `f`.
58    fn scope<'env, 'scope, F>(&'env self, f: F)
59    where
60        'env: 'scope,
61        for<'s> F: FnOnce(Self::ScopeRef<'s, 'env, 'scope>) + Send;
62
63    /// Returns the maximum number of threads available in the pool.
64    ///
65    /// This value reflects all constraints applied up to pool creation time, including:
66    /// - The requested thread count from pool construction
67    /// - The `ORX_NUM_THREADS` environment variable (if set)
68    /// - The system's available CPU cores
69    ///
70    /// Individual computations can further limit this via `.num_threads()` method.
71    fn max_num_threads(&self) -> NonZeroUsize;
72
73    /// Runs all `tasks` in parallel on this pool.
74    ///
75    /// `tasks` is a statically typed [`TaskQueue`]: pushed tasks are stored inline,
76    /// requiring no object safety, boxing or heap allocation. None of the tasks start
77    /// running until `run_all` is called.
78    ///
79    /// Tasks can be created using the [`tasks!`] macro, or fluently via [`Tasks::new`] and [`push`].
80    ///
81    /// [`push`]: crate::pools::tasks::TaskQueue::push
82    /// [`Tasks::new`]: crate::Tasks::new
83    /// [`tasks!`]: crate::tasks
84    ///
85    /// # Example
86    ///
87    /// ```rust
88    /// use orx_parallel::*;
89    ///
90    /// let work_for = |n| std::thread::sleep(std::time::Duration::from_millis(n));
91    ///
92    /// let tasks = tasks![
93    ///     || {
94    ///         work_for(90);
95    ///         println!("t1 completes 4th");
96    ///     },
97    ///     || println!("t2 completes 1st"),
98    ///     || {
99    ///         work_for(10);
100    ///         println!("t3 completes 2nd");
101    ///     },
102    ///     || {
103    ///         work_for(50);
104    ///         println!("t4 completes 3rd");
105    ///     },
106    /// ];
107    ///
108    /// Pool::global().run_all(tasks);
109    ///
110    /// // prints:
111    /// // t2 completes 1st
112    /// // t3 completes 2nd
113    /// // t4 completes 3rd
114    /// // t1 completes 4th
115    /// ```
116    ///
117    /// Below is a more practical example: computing independent statistics over the same
118    /// input concurrently and collecting the results:
119    ///
120    /// ```rust
121    /// use orx_parallel::*;
122    /// use std::sync::Mutex;
123    ///
124    /// let numbers = [4, 8, 15, 16, 23, 42];
125    ///
126    /// let sum = Mutex::new(0);
127    /// let max = Mutex::new(i32::MIN);
128    /// let all_positive = Mutex::new(false);
129    ///
130    /// let tasks = tasks![
131    ///     || *sum.lock().unwrap() = numbers.iter().sum(),
132    ///     || *max.lock().unwrap() = numbers.iter().copied().max().unwrap(),
133    ///     || *all_positive.lock().unwrap() = numbers.iter().all(|&x| x > 0),
134    /// ];
135    ///
136    /// Pool::global().run_all(tasks);
137    ///
138    /// println!(
139    ///     "sum={}, max={}, all_positive={}",
140    ///     sum.into_inner().unwrap(),
141    ///     max.into_inner().unwrap(),
142    ///     all_positive.into_inner().unwrap(),
143    /// );
144    /// ```
145    ///
146    /// Tasks can also be created fluently via [`Tasks::new`]:
147    ///
148    /// ```rust
149    /// use orx_parallel::*;
150    ///
151    /// let tasks = Tasks::new()
152    ///     .push(|| println!("task 1"))
153    ///     .push(|| println!("task 2"));
154    ///
155    /// Pool::global().run_all(tasks);
156    /// ```
157    fn run_all(&self, tasks: impl TaskQueue + Send) {
158        self.scope(|s| tasks.run(s));
159    }
160}
161
162/// Calculates the actual thread count for a computation considering multiple constraints.
163///
164/// This method implements the core thread count decision logic by combining:
165///
166/// 1. **Pool constraint** (`self.max_num_threads()`)
167///    - The thread pool's maximum capacity
168///    - Already includes environment variable constraints
169///
170/// 2. **Computation constraint** (`params.num_threads`)
171///    - Per-computation request from `.num_threads()` method
172///    - Can be `NumThreads::Auto` (use all available)
173///    - Or `NumThreads::Max(n)` (hard limit)
174///
175/// 3. **Input size constraint** (known upper bound from `size_hint.1`)
176///    - Cannot spawn more threads than input elements
177///    - When input size is unknown (None), this constraint doesn't apply
178///
179/// # Returns
180///
181/// The minimum of all constraints, representing the actual thread count to use.
182///
183/// # Decision Logic
184///
185/// ```text
186/// let available = self.max_num_threads()           // Pool limit
187///
188/// let requested = match (size_hint.1, params.num_threads) {
189///     (Some(len), Auto) => min(len, MaxUsize),     // Cap by input size
190///     (Some(len), Max(n)) => min(len, n),          // Cap by input size and request
191///     (None, Auto) => MaxUsize,                    // No constraints
192///     (None, Max(n)) => n,                         // Only respect request
193/// };
194///
195/// return min(requested, available)                 // Final decision
196/// ```
197///
198/// # Parameters
199///
200/// - `params` - Contains `.num_threads` setting from `.num_threads()` method
201/// - `size_hint` - Tuple of (lower_bound, Option<upper_bound>) for input size
202///   - If upper_bound is `None`, input size is unknown
203///   - If upper_bound is `Some(n)`, input has at most n elements
204pub fn max_num_threads_for_computation(
205    pool: &impl ThreadPool,
206    params: Params,
207    size_hint: (usize, Option<usize>),
208) -> usize {
209    let ava = pool.max_num_threads();
210
211    let req = match (size_hint.1, params.num_threads) {
212        (Some(len_ub), NumThreads::Auto) => non_zero_or_one(len_ub),
213        (Some(len_ub), NumThreads::Max(nt)) => non_zero_or_one(len_ub).min(nt),
214        (None, NumThreads::Auto) => NonZeroUsize::MAX,
215        (None, NumThreads::Max(nt)) => nt,
216    };
217
218    core::cmp::min(req, ava).into()
219}