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}