Skip to main content

ocas_core/
thread_pool.rs

1//! Thread pool utilities.
2//!
3//! oCAS uses `rayon` for data parallelism. This module provides convenience
4//! helpers and configuration defaults.
5
6pub use rayon::prelude::*;
7
8use crate::error::{OcasError, Result};
9
10/// A scoped thread pool wrapper around `rayon::ThreadPool`.
11///
12/// Unlike the global pool, a `ThreadPool` can be created and destroyed
13/// independently, which is useful for short-lived parallel tasks or tests.
14///
15/// # Example
16///
17/// ```
18/// use ocas_core::thread_pool::ThreadPool;
19///
20/// let pool = ThreadPool::new(2).expect("failed to create pool");
21/// let result = pool.install(|| {
22///     let (a, b) = rayon::join(|| 1 + 2, || 3 + 4);
23///     a + b
24/// });
25/// assert_eq!(result, 10);
26/// ```
27pub struct ThreadPool {
28    inner: rayon::ThreadPool,
29}
30
31impl ThreadPool {
32    /// Create a new thread pool with the given number of worker threads.
33    ///
34    /// # Errors
35    ///
36    /// Returns `OcasError::BackendError` if rayon fails to build the pool
37    /// (for example, because the global pool was already initialized with a
38    /// different configuration).
39    pub fn new(threads: usize) -> Result<Self> {
40        let inner = rayon::ThreadPoolBuilder::new()
41            .num_threads(threads)
42            .build()
43            .map_err(|e| OcasError::BackendError {
44                backend: "rayon".into(),
45                message: e.to_string(),
46            })?;
47        Ok(Self { inner })
48    }
49
50    /// Execute a closure in this thread pool.
51    pub fn install<F, R>(&self, op: F) -> R
52    where
53        F: FnOnce() -> R + Send,
54        R: Send,
55    {
56        self.inner.install(op)
57    }
58
59    /// Return the number of worker threads in this pool.
60    pub fn current_num_threads(&self) -> usize {
61        self.inner.current_num_threads()
62    }
63}
64
65/// Initialize the global thread pool with the default configuration.
66///
67/// This is normally called automatically by `rayon`, but explicit
68/// initialization allows setting the number of threads.
69///
70/// # Errors
71///
72/// Returns `OcasError::BackendError` if the global pool was already
73/// initialized with a different configuration.
74pub fn init(threads: usize) -> Result<()> {
75    rayon::ThreadPoolBuilder::new()
76        .num_threads(threads)
77        .build_global()
78        .map_err(|e| OcasError::BackendError {
79            backend: "rayon".into(),
80            message: e.to_string(),
81        })
82}
83
84/// Execute a closure in the global thread pool.
85///
86/// # Example
87///
88/// ```
89/// use ocas_core::thread_pool::install;
90///
91/// let result = install(|| 21 + 21);
92/// assert_eq!(result, 42);
93/// ```
94pub fn install<F, R>(op: F) -> R
95where
96    F: FnOnce() -> R + Send,
97    R: Send,
98{
99    rayon::join(|| {}, op).1
100}
101
102#[cfg(test)]
103mod tests {
104    use super::*;
105
106    mod simple {
107        use super::*;
108
109        #[test]
110        #[cfg(not(miri))]
111        fn global_pool_install() {
112            let result = install(|| 21 + 21);
113            assert_eq!(result, 42);
114        }
115
116        #[test]
117        #[cfg(not(miri))]
118        fn thread_pool_creation_and_size() {
119            let pool = ThreadPool::new(2).expect("should create a 2-thread pool");
120            assert_eq!(pool.current_num_threads(), 2);
121        }
122    }
123
124    mod medium {
125        use super::*;
126
127        #[test]
128        #[cfg(not(miri))]
129        fn custom_thread_pool_join() {
130            let pool = ThreadPool::new(2).expect("should create a 2-thread pool");
131            let result = pool.install(|| {
132                let (a, b) = rayon::join(|| 1 + 2, || 3 + 4);
133                a + b
134            });
135            assert_eq!(result, 10);
136        }
137
138        #[test]
139        #[cfg(not(miri))]
140        fn thread_pool_parallel_sum() {
141            let pool = ThreadPool::new(2).expect("should create a 2-thread pool");
142            let data: Vec<i64> = (0..1_000).collect();
143            let sum = pool.install(|| data.into_par_iter().sum::<i64>());
144            assert_eq!(sum, 499_500);
145        }
146    }
147
148    mod complex {
149        use super::*;
150
151        #[test]
152        #[cfg(not(miri))]
153        fn thread_pool_maps_error_to_ocas_error() {
154            // Initialize the global pool first so that building a new pool cannot
155            // conflict. Then attempt to re-initialize with a different size, which
156            // must fail.
157            init(2).ok();
158            let err = init(4).expect_err("reinitializing global pool should fail");
159            assert_eq!(
160                err,
161                OcasError::BackendError {
162                    backend: "rayon".into(),
163                    message: "The global thread pool has already been initialized.".into(),
164                }
165            );
166        }
167
168        #[test]
169        #[cfg(not(miri))]
170        fn nested_pools_run_on_different_threads() {
171            let outer = ThreadPool::new(2).expect("outer pool");
172            let result = outer.install(|| {
173                let inner = ThreadPool::new(2).expect("inner pool");
174                inner.install(|| {
175                    let (a, b) = rayon::join(|| 1 + 1, || 2 + 2);
176                    a + b
177                })
178            });
179            assert_eq!(result, 6);
180        }
181    }
182
183    mod extreme {
184        use super::*;
185
186        #[test]
187        #[cfg(not(miri))]
188        fn stress_many_parallel_tasks() {
189            let pool = ThreadPool::new(4).expect("should create a 4-thread pool");
190            let result = pool.install(|| (0..10_000).into_par_iter().map(|x| x * x).sum::<i64>());
191            // Sum of squares 0^2 + ... + (n-1)^2 = (n-1)n(2n-1)/6
192            let n = 10_000i64;
193            assert_eq!(result, (n - 1) * n * (2 * n - 1) / 6);
194        }
195    }
196}