Skip to main content

sklears_utils/
parallel.rs

1//! Parallel computing utilities for machine learning workloads
2//!
3//! This module provides utilities for parallel processing, including thread pool
4//! management, work-stealing algorithms, and parallel iterator utilities.
5
6use crate::{UtilsError, UtilsResult};
7use scirs2_core::numeric::Zero;
8use std::collections::VecDeque;
9use std::sync::{Arc, Mutex};
10use std::thread;
11#[cfg(test)]
12use std::time::Duration;
13
14/// Thread pool for parallel task execution
15#[derive(Debug)]
16pub struct ThreadPool {
17    workers: Vec<Worker>,
18    sender: Option<std::sync::mpsc::Sender<Job>>,
19    num_threads: usize,
20}
21
22type Job = Box<dyn FnOnce() + Send + 'static>;
23
24impl ThreadPool {
25    /// Create a new thread pool with the specified number of threads
26    pub fn new(num_threads: usize) -> UtilsResult<Self> {
27        if num_threads == 0 {
28            return Err(UtilsError::InvalidParameter(
29                "Thread pool size must be greater than 0".to_string(),
30            ));
31        }
32
33        let (sender, receiver) = std::sync::mpsc::channel();
34        let receiver = Arc::new(Mutex::new(receiver));
35        let mut workers = Vec::with_capacity(num_threads);
36
37        for id in 0..num_threads {
38            workers.push(Worker::new(id, Arc::clone(&receiver))?);
39        }
40
41        Ok(ThreadPool {
42            workers,
43            sender: Some(sender),
44            num_threads,
45        })
46    }
47
48    /// Create a thread pool with number of threads equal to CPU cores
49    pub fn with_cpu_cores() -> UtilsResult<Self> {
50        let num_cores = num_cpus::get();
51        Self::new(num_cores)
52    }
53
54    /// Submit a job to the thread pool
55    pub fn execute<F>(&self, f: F) -> UtilsResult<()>
56    where
57        F: FnOnce() + Send + 'static,
58    {
59        let job = Box::new(f);
60        self.sender
61            .as_ref()
62            .ok_or_else(|| {
63                UtilsError::InvalidParameter("Thread pool is shutting down".to_string())
64            })?
65            .send(job)
66            .map_err(|_| {
67                UtilsError::InvalidParameter("Failed to send job to thread pool".to_string())
68            })?;
69        Ok(())
70    }
71
72    /// Get the number of worker threads
73    pub fn thread_count(&self) -> usize {
74        self.num_threads
75    }
76
77    /// Wait for all current jobs to complete
78    pub fn join(&mut self) {
79        drop(self.sender.take());
80        for worker in &mut self.workers {
81            if let Some(thread) = worker.thread.take() {
82                thread.join().expect("operation should succeed");
83            }
84        }
85    }
86}
87
88impl Drop for ThreadPool {
89    fn drop(&mut self) {
90        drop(self.sender.take());
91        for worker in &mut self.workers {
92            if let Some(thread) = worker.thread.take() {
93                thread.join().expect("operation should succeed");
94            }
95        }
96    }
97}
98
99#[derive(Debug)]
100struct Worker {
101    #[allow(dead_code)]
102    id: usize,
103    thread: Option<thread::JoinHandle<()>>,
104}
105
106impl Worker {
107    fn new(id: usize, receiver: Arc<Mutex<std::sync::mpsc::Receiver<Job>>>) -> UtilsResult<Self> {
108        let thread = thread::spawn(move || loop {
109            let job = receiver.lock().expect("operation should succeed").recv();
110            match job {
111                Ok(job) => {
112                    job();
113                }
114                Err(_) => {
115                    break;
116                }
117            }
118        });
119
120        Ok(Worker {
121            id,
122            thread: Some(thread),
123        })
124    }
125}
126
127/// Work-stealing deque for load balancing
128#[derive(Debug)]
129pub struct WorkStealingQueue<T> {
130    local_queue: Arc<Mutex<VecDeque<T>>>,
131    global_queue: Arc<Mutex<VecDeque<T>>>,
132    workers: Vec<Arc<Mutex<VecDeque<T>>>>,
133    worker_id: usize,
134}
135
136impl<T> WorkStealingQueue<T>
137where
138    T: Send + 'static + Clone,
139{
140    /// Create a new work-stealing queue system
141    pub fn new(num_workers: usize) -> Self {
142        let global_queue = Arc::new(Mutex::new(VecDeque::new()));
143        let mut workers = Vec::with_capacity(num_workers);
144
145        for _ in 0..num_workers {
146            workers.push(Arc::new(Mutex::new(VecDeque::new())));
147        }
148
149        Self {
150            local_queue: Arc::clone(&workers[0]),
151            global_queue,
152            workers,
153            worker_id: 0,
154        }
155    }
156
157    /// Push a task to the local queue
158    pub fn push_local(&self, task: T) -> UtilsResult<()> {
159        self.local_queue
160            .lock()
161            .map_err(|_| {
162                UtilsError::InvalidParameter("Failed to acquire local queue lock".to_string())
163            })?
164            .push_back(task);
165        Ok(())
166    }
167
168    /// Push a task to the global queue
169    pub fn push_global(&self, task: T) -> UtilsResult<()> {
170        self.global_queue
171            .lock()
172            .map_err(|_| {
173                UtilsError::InvalidParameter("Failed to acquire global queue lock".to_string())
174            })?
175            .push_back(task);
176        Ok(())
177    }
178
179    /// Pop a task from the local queue
180    pub fn pop_local(&self) -> UtilsResult<Option<T>> {
181        Ok(self
182            .local_queue
183            .lock()
184            .map_err(|_| {
185                UtilsError::InvalidParameter("Failed to acquire local queue lock".to_string())
186            })?
187            .pop_front())
188    }
189
190    /// Steal work from other workers' queues
191    pub fn steal_work(&self) -> UtilsResult<Option<T>> {
192        // Try to steal from other workers' queues
193        for (i, worker_queue) in self.workers.iter().enumerate() {
194            if i != self.worker_id {
195                if let Ok(mut queue) = worker_queue.try_lock() {
196                    if let Some(task) = queue.pop_back() {
197                        return Ok(Some(task));
198                    }
199                }
200            }
201        }
202
203        // If no work was stolen, try the global queue
204        if let Ok(mut global) = self.global_queue.try_lock() {
205            return Ok(global.pop_front());
206        }
207
208        Ok(None)
209    }
210
211    /// Get the next task, trying local queue first, then stealing
212    pub fn get_task(&self) -> UtilsResult<Option<T>> {
213        // Try local queue first
214        if let Some(task) = self.pop_local()? {
215            return Ok(Some(task));
216        }
217
218        // If local queue is empty, try stealing
219        self.steal_work()
220    }
221}
222
223/// Parallel iterator utilities
224pub struct ParallelIterator<T> {
225    items: Vec<T>,
226    chunk_size: usize,
227}
228
229impl<T> ParallelIterator<T>
230where
231    T: Send + 'static + Clone,
232{
233    /// Create a new parallel iterator
234    pub fn new(items: Vec<T>) -> Self {
235        let chunk_size = (items.len() / num_cpus::get()).max(1);
236        Self { items, chunk_size }
237    }
238
239    /// Set the chunk size for parallel processing
240    pub fn with_chunk_size(mut self, chunk_size: usize) -> Self {
241        self.chunk_size = chunk_size.max(1);
242        self
243    }
244
245    /// Map function over items in parallel
246    pub fn map<F, R>(self, f: F) -> UtilsResult<Vec<R>>
247    where
248        F: Fn(T) -> R + Send + Sync + 'static,
249        R: Send + 'static + Clone,
250    {
251        let f = Arc::new(f);
252        let results = Arc::new(Mutex::new(Vec::with_capacity(self.items.len())));
253        let _thread_pool = ThreadPool::with_cpu_cores()?;
254
255        // Split items into chunks
256        let chunks: Vec<_> = self
257            .items
258            .into_iter()
259            .collect::<Vec<_>>()
260            .chunks(self.chunk_size)
261            .map(|chunk| chunk.to_vec())
262            .collect();
263
264        let mut handles = Vec::new();
265
266        for (chunk_idx, chunk) in chunks.into_iter().enumerate() {
267            let f_clone = Arc::clone(&f);
268            let results_clone = Arc::clone(&results);
269            let chunk_size = chunk.len();
270
271            let handle = thread::spawn(move || {
272                let mut chunk_results = Vec::with_capacity(chunk_size);
273                for item in chunk {
274                    chunk_results.push(f_clone(item));
275                }
276
277                let mut results_lock = results_clone.lock().expect("operation should succeed");
278                // Ensure we have enough space
279                if results_lock.len() <= chunk_idx {
280                    results_lock.resize_with(chunk_idx + 1, || Vec::new());
281                }
282                results_lock[chunk_idx] = chunk_results;
283            });
284
285            handles.push(handle);
286        }
287
288        // Wait for all threads to complete
289        for handle in handles {
290            handle.join().map_err(|_| {
291                UtilsError::InvalidParameter(
292                    "Thread panicked during parallel execution".to_string(),
293                )
294            })?;
295        }
296
297        // Collect results in order
298        let results_lock = results.lock().expect("operation should succeed");
299        let mut final_results = Vec::new();
300        for chunk_results in results_lock.iter() {
301            final_results.extend_from_slice(chunk_results);
302        }
303
304        Ok(final_results)
305    }
306
307    /// Filter items in parallel
308    pub fn filter<F>(self, predicate: F) -> UtilsResult<Vec<T>>
309    where
310        F: Fn(&T) -> bool + Send + Sync + 'static,
311        T: Clone,
312    {
313        let predicate = Arc::new(predicate);
314        let results = Arc::new(Mutex::new(Vec::new()));
315        let _thread_pool = ThreadPool::with_cpu_cores()?;
316
317        // Split items into chunks
318        let chunks: Vec<_> = self
319            .items
320            .into_iter()
321            .collect::<Vec<_>>()
322            .chunks(self.chunk_size)
323            .map(|chunk| chunk.to_vec())
324            .collect();
325
326        let mut handles = Vec::new();
327
328        for (chunk_idx, chunk) in chunks.into_iter().enumerate() {
329            let predicate_clone = Arc::clone(&predicate);
330            let results_clone = Arc::clone(&results);
331
332            let handle = thread::spawn(move || {
333                let filtered: Vec<T> = chunk
334                    .into_iter()
335                    .filter(|item| predicate_clone(item))
336                    .collect();
337
338                let mut results_lock = results_clone.lock().expect("operation should succeed");
339                // Ensure we have enough space
340                if results_lock.len() <= chunk_idx {
341                    results_lock.resize_with(chunk_idx + 1, || Vec::new());
342                }
343                results_lock[chunk_idx] = filtered;
344            });
345
346            handles.push(handle);
347        }
348
349        // Wait for all threads to complete
350        for handle in handles {
351            handle.join().map_err(|_| {
352                UtilsError::InvalidParameter(
353                    "Thread panicked during parallel execution".to_string(),
354                )
355            })?;
356        }
357
358        // Collect results in order
359        let results_lock = results.lock().expect("operation should succeed");
360        let mut final_results = Vec::new();
361        for chunk_results in results_lock.iter() {
362            final_results.extend_from_slice(chunk_results);
363        }
364
365        Ok(final_results)
366    }
367}
368
369/// Parallel reduction operations
370pub struct ParallelReducer;
371
372impl ParallelReducer {
373    /// Reduce a vector in parallel using the given operation
374    pub fn reduce<T, F>(items: Vec<T>, initial: T, op: F) -> UtilsResult<T>
375    where
376        T: Send + Sync + Clone + 'static,
377        F: Fn(T, T) -> T + Send + Sync + 'static,
378    {
379        if items.is_empty() {
380            return Ok(initial);
381        }
382
383        let op = Arc::new(op);
384        let chunk_size = (items.len() / num_cpus::get()).max(1);
385
386        // Split items into chunks
387        let chunks: Vec<_> = items
388            .chunks(chunk_size)
389            .map(|chunk| chunk.to_vec())
390            .collect();
391
392        let mut handles = Vec::new();
393        let mut partial_results = Vec::new();
394
395        for chunk in chunks.into_iter() {
396            let op_clone = Arc::clone(&op);
397            let initial_clone = initial.clone();
398
399            let handle = thread::spawn(move || {
400                chunk
401                    .into_iter()
402                    .fold(initial_clone, |acc, item| op_clone(acc, item))
403            });
404
405            handles.push(handle);
406        }
407
408        // Collect partial results
409        for handle in handles {
410            let result = handle.join().map_err(|_| {
411                UtilsError::InvalidParameter(
412                    "Thread panicked during parallel reduction".to_string(),
413                )
414            })?;
415            partial_results.push(result);
416        }
417
418        // Reduce partial results
419        Ok(partial_results
420            .into_iter()
421            .fold(initial, |acc, partial| op(acc, partial)))
422    }
423
424    /// Sum elements in parallel
425    pub fn sum<T>(items: Vec<T>) -> UtilsResult<T>
426    where
427        T: Send + Sync + Clone + std::ops::Add<Output = T> + Zero + 'static,
428    {
429        Self::reduce(items, T::zero(), |a, b| a + b)
430    }
431
432    /// Find minimum element in parallel
433    pub fn min<T>(items: Vec<T>) -> UtilsResult<Option<T>>
434    where
435        T: Send + Sync + Clone + Ord + 'static,
436    {
437        if items.is_empty() {
438            return Ok(None);
439        }
440
441        let first = items[0].clone();
442        let result = Self::reduce(items, first, |a, b| if a < b { a } else { b })?;
443        Ok(Some(result))
444    }
445
446    /// Find maximum element in parallel
447    pub fn max<T>(items: Vec<T>) -> UtilsResult<Option<T>>
448    where
449        T: Send + Sync + Clone + Ord + 'static,
450    {
451        if items.is_empty() {
452            return Ok(None);
453        }
454
455        let first = items[0].clone();
456        let result = Self::reduce(items, first, |a, b| if a > b { a } else { b })?;
457        Ok(Some(result))
458    }
459}
460
461#[allow(non_snake_case)]
462#[cfg(test)]
463mod tests {
464    use super::*;
465    use std::sync::atomic::{AtomicUsize, Ordering};
466
467    #[test]
468    fn test_thread_pool_creation() {
469        let pool = ThreadPool::new(4).expect("operation should succeed");
470        assert_eq!(pool.thread_count(), 4);
471    }
472
473    #[test]
474    fn test_thread_pool_execution() {
475        let pool = ThreadPool::new(2).expect("operation should succeed");
476        let counter = Arc::new(AtomicUsize::new(0));
477
478        for _ in 0..10 {
479            let counter_clone = Arc::clone(&counter);
480            pool.execute(move || {
481                counter_clone.fetch_add(1, Ordering::SeqCst);
482            })
483            .expect("operation should succeed");
484        }
485
486        // Give threads time to complete
487        thread::sleep(Duration::from_millis(100));
488
489        assert_eq!(counter.load(Ordering::SeqCst), 10);
490    }
491
492    #[test]
493    fn test_work_stealing_queue() {
494        let queue = WorkStealingQueue::new(4);
495
496        queue.push_local(42).expect("operation should succeed");
497        queue.push_global(24).expect("operation should succeed");
498
499        assert_eq!(
500            queue.get_task().expect("operation should succeed"),
501            Some(42)
502        );
503        assert_eq!(
504            queue.get_task().expect("operation should succeed"),
505            Some(24)
506        );
507        assert_eq!(queue.get_task().expect("operation should succeed"), None);
508    }
509
510    #[test]
511    fn test_parallel_iterator_map() {
512        let items = vec![1, 2, 3, 4, 5];
513        let iter = ParallelIterator::new(items);
514
515        let results = iter.map(|x| x * 2).expect("operation should succeed");
516        assert_eq!(results, vec![2, 4, 6, 8, 10]);
517    }
518
519    #[test]
520    fn test_parallel_iterator_filter() {
521        let items = vec![1, 2, 3, 4, 5, 6];
522        let iter = ParallelIterator::new(items);
523
524        let results = iter
525            .filter(|&x| x % 2 == 0)
526            .expect("operation should succeed");
527        assert_eq!(results, vec![2, 4, 6]);
528    }
529
530    #[test]
531    fn test_parallel_reducer_sum() {
532        let items = vec![1, 2, 3, 4, 5];
533        let result = ParallelReducer::sum(items).expect("operation should succeed");
534        assert_eq!(result, 15);
535    }
536
537    #[test]
538    fn test_parallel_reducer_min_max() {
539        let items = vec![5, 2, 8, 1, 9, 3];
540
541        let min_result = ParallelReducer::min(items.clone()).expect("operation should succeed");
542        assert_eq!(min_result, Some(1));
543
544        let max_result = ParallelReducer::max(items).expect("operation should succeed");
545        assert_eq!(max_result, Some(9));
546    }
547
548    #[test]
549    fn test_parallel_reducer_empty() {
550        let items: Vec<i32> = vec![];
551
552        let min_result = ParallelReducer::min(items.clone()).expect("operation should succeed");
553        assert_eq!(min_result, None);
554
555        let max_result = ParallelReducer::max(items).expect("operation should succeed");
556        assert_eq!(max_result, None);
557    }
558
559    #[test]
560    fn test_thread_pool_with_cpu_cores() {
561        let pool = ThreadPool::with_cpu_cores().expect("operation should succeed");
562        assert!(pool.thread_count() > 0);
563        assert!(pool.thread_count() <= num_cpus::get());
564    }
565}