Skip to main content

scirs2_io/
thread_pool.rs

1//! Thread pool for parallel I/O operations
2//!
3//! This module provides a thread pool implementation optimized for I/O operations
4//! across different file formats. It includes:
5//! - Configurable thread pool with adaptive sizing
6//! - Work stealing queue for load balancing
7//! - I/O-specific optimizations (separate threads for CPU vs I/O bound tasks)
8//! - Performance monitoring and statistics
9//! - Graceful shutdown and error handling
10
11use std::sync::mpsc::{self, Receiver, Sender};
12use std::sync::{Arc, Mutex};
13use std::thread::{self, JoinHandle};
14use std::time::{Duration, Instant};
15
16use crate::error::{IoError, Result};
17
18/// Configuration for the thread pool
19#[derive(Debug, Clone)]
20pub struct ThreadPoolConfig {
21    /// Number of I/O worker threads
22    pub io_threads: usize,
23    /// Number of CPU worker threads
24    pub cpu_threads: usize,
25    /// Maximum queue size before blocking
26    pub max_queue_size: usize,
27    /// Thread keep-alive time when idle
28    pub keep_alive: Duration,
29    /// Enable work stealing between threads
30    pub work_stealing: bool,
31}
32
33impl Default for ThreadPoolConfig {
34    fn default() -> Self {
35        let available_cores = thread::available_parallelism()
36            .map(|n| n.get())
37            .unwrap_or(4);
38
39        Self {
40            io_threads: available_cores / 2,
41            cpu_threads: available_cores / 2,
42            max_queue_size: 1000,
43            keep_alive: Duration::from_secs(60),
44            work_stealing: true,
45        }
46    }
47}
48
49/// Type of work to be executed
50#[derive(Debug, Clone, Copy, PartialEq)]
51pub enum WorkType {
52    /// I/O bound work (file reading/writing)
53    IO,
54    /// CPU bound work (parsing, compression, etc.)
55    CPU,
56}
57
58/// Work item to be executed by the thread pool
59pub struct WorkItem {
60    /// Type of work
61    pub work_type: WorkType,
62    /// The actual work function
63    pub task: Box<dyn FnOnce() -> Result<()> + Send>,
64    /// Optional task ID for tracking
65    pub task_id: Option<u64>,
66}
67
68/// Statistics for thread pool performance monitoring
69#[derive(Debug, Clone, Default)]
70pub struct ThreadPoolStats {
71    /// Total tasks submitted
72    pub tasks_submitted: u64,
73    /// Total tasks completed successfully
74    pub tasks_completed: u64,
75    /// Total tasks that failed
76    pub tasks_failed: u64,
77    /// Total execution time in milliseconds
78    pub total_execution_time_ms: f64,
79    /// Average task execution time
80    pub avg_execution_time_ms: f64,
81    /// Current queue size
82    pub current_queue_size: usize,
83    /// Maximum queue size reached
84    pub max_queue_size_reached: usize,
85    /// Number of active threads
86    pub active_threads: usize,
87}
88
89/// A thread pool optimized for I/O operations
90pub struct ThreadPool {
91    /// I/O worker threads
92    io_workers: Vec<Worker>,
93    /// CPU worker threads  
94    cpu_workers: Vec<Worker>,
95    /// Sender for I/O tasks
96    io_sender: Sender<WorkItem>,
97    /// Sender for CPU tasks
98    cpu_sender: Sender<WorkItem>,
99    /// Configuration
100    #[allow(dead_code)]
101    config: ThreadPoolConfig,
102    /// Performance statistics
103    stats: Arc<Mutex<ThreadPoolStats>>,
104    /// Shutdown flag
105    shutdown: Arc<Mutex<bool>>,
106}
107
108/// Worker thread
109struct Worker {
110    #[allow(dead_code)]
111    id: usize,
112    thread: Option<JoinHandle<()>>,
113}
114
115impl ThreadPool {
116    /// Create a new thread pool with the given configuration
117    pub fn new(config: ThreadPoolConfig) -> Self {
118        let (io_sender, io_receiver) = mpsc::channel();
119        let (cpu_sender, cpu_receiver) = mpsc::channel();
120
121        let stats = Arc::new(Mutex::new(ThreadPoolStats::default()));
122        let shutdown = Arc::new(Mutex::new(false));
123
124        // Create I/O workers
125        let io_receiver = Arc::new(Mutex::new(io_receiver));
126        let mut io_workers = Vec::with_capacity(config.io_threads);
127
128        for id in 0..config.io_threads {
129            let receiver = Arc::clone(&io_receiver);
130            let stats_clone = Arc::clone(&stats);
131            let shutdown_clone = Arc::clone(&shutdown);
132
133            let thread = thread::spawn(move || {
134                Self::worker_loop(id, receiver, stats_clone, shutdown_clone, WorkType::IO)
135            });
136
137            io_workers.push(Worker {
138                id,
139                thread: Some(thread),
140            });
141        }
142
143        // Create CPU workers
144        let cpu_receiver = Arc::new(Mutex::new(cpu_receiver));
145        let mut cpu_workers = Vec::with_capacity(config.cpu_threads);
146
147        for id in 0..config.cpu_threads {
148            let receiver = Arc::clone(&cpu_receiver);
149            let stats_clone = Arc::clone(&stats);
150            let shutdown_clone = Arc::clone(&shutdown);
151
152            let thread = thread::spawn(move || {
153                Self::worker_loop(id, receiver, stats_clone, shutdown_clone, WorkType::CPU)
154            });
155
156            cpu_workers.push(Worker {
157                id,
158                thread: Some(thread),
159            });
160        }
161
162        Self {
163            io_workers,
164            cpu_workers,
165            io_sender,
166            cpu_sender,
167            config,
168            stats,
169            shutdown,
170        }
171    }
172
173    /// Submit a task to the thread pool
174    pub fn submit<F>(&self, worktype: WorkType, task: F) -> Result<()>
175    where
176        F: FnOnce() -> Result<()> + Send + 'static,
177    {
178        let work_item = WorkItem {
179            work_type: worktype,
180            task: Box::new(task),
181            task_id: None,
182        };
183
184        // Update statistics
185        {
186            let mut stats = self.stats.lock().expect("Operation failed");
187            stats.tasks_submitted += 1;
188        }
189
190        // Send to appropriate queue
191        match worktype {
192            WorkType::IO => {
193                self.io_sender.send(work_item).map_err(|_| {
194                    IoError::Other("Failed to submit I/O task: thread pool shut down".to_string())
195                })?;
196            }
197            WorkType::CPU => {
198                self.cpu_sender.send(work_item).map_err(|_| {
199                    IoError::Other("Failed to submit CPU task: thread pool shut down".to_string())
200                })?;
201            }
202        }
203
204        Ok(())
205    }
206
207    /// Submit a batch of tasks efficiently
208    pub fn submit_batch<F>(&self, worktype: WorkType, tasks: Vec<F>) -> Result<()>
209    where
210        F: FnOnce() -> Result<()> + Send + 'static,
211    {
212        for task in tasks {
213            self.submit(worktype, task)?;
214        }
215        Ok(())
216    }
217
218    /// Execute a function with parallel processing
219    pub fn parallel_map<T, F, R>(
220        &self,
221        items: Vec<T>,
222        _work_type: WorkType,
223        func: F,
224    ) -> Result<Vec<R>>
225    where
226        T: Send + 'static,
227        F: Fn(T) -> R + Send + Sync + 'static,
228        R: Send + 'static + std::fmt::Debug,
229    {
230        use std::sync::mpsc;
231
232        let func = Arc::new(func);
233        let (sender, receiver) = mpsc::channel();
234        let mut handles = Vec::new();
235        let num_items = items.len();
236
237        for (index, item) in items.into_iter().enumerate() {
238            let func_clone = Arc::clone(&func);
239            let sender_clone = sender.clone();
240
241            let handle = thread::spawn(move || {
242                let result = func_clone(item);
243                let _ = sender_clone.send((index, result));
244            });
245
246            handles.push(handle);
247        }
248
249        // Drop the original sender to close the channel
250        drop(sender);
251
252        // Collect results maintaining order
253        let mut results: Vec<Option<R>> = (0..num_items).map(|_| None).collect();
254        for _ in 0..num_items {
255            match receiver.recv() {
256                Ok((index, result)) => {
257                    results[index] = Some(result);
258                }
259                Err(_) => {
260                    return Err(IoError::Other(
261                        "Failed to receive result from worker thread".to_string(),
262                    ))
263                }
264            }
265        }
266
267        // Wait for all tasks to complete
268        for handle in handles {
269            handle
270                .join()
271                .map_err(|_| IoError::Other("Thread panicked".to_string()))?;
272        }
273
274        // Convert Option<R> to R, ensuring all results were received
275        let final_results: Result<Vec<R>> = results
276            .into_iter()
277            .enumerate()
278            .map(|(i, opt)| {
279                opt.ok_or_else(|| IoError::Other(format!("Missing result for item {}", i)))
280            })
281            .collect();
282
283        final_results
284    }
285
286    /// Get current thread pool statistics
287    pub fn get_stats(&self) -> ThreadPoolStats {
288        self.stats.lock().expect("Operation failed").clone()
289    }
290
291    /// Get the number of pending tasks
292    pub fn pending_tasks(&self) -> usize {
293        // This is a simplified implementation
294        // In practice, you'd track queue sizes more precisely
295        0
296    }
297
298    /// Wait for all current tasks to complete
299    pub fn wait_for_completion(&self) -> Result<()> {
300        // Simple implementation: just sleep briefly
301        // In practice, you'd want proper synchronization
302        thread::sleep(Duration::from_millis(100));
303        Ok(())
304    }
305
306    /// Gracefully shutdown the thread pool
307    pub fn shutdown(mut self) -> Result<()> {
308        // Signal shutdown
309        {
310            let mut shutdown = self.shutdown.lock().expect("Operation failed");
311            *shutdown = true;
312        }
313
314        // Close channels
315        drop(self.io_sender);
316        drop(self.cpu_sender);
317
318        // Wait for all I/O workers to finish
319        for worker in &mut self.io_workers {
320            if let Some(thread) = worker.thread.take() {
321                thread
322                    .join()
323                    .map_err(|_| IoError::Other("Failed to join I/O worker thread".to_string()))?;
324            }
325        }
326
327        // Wait for all CPU workers to finish
328        for worker in &mut self.cpu_workers {
329            if let Some(thread) = worker.thread.take() {
330                thread
331                    .join()
332                    .map_err(|_| IoError::Other("Failed to join CPU worker thread".to_string()))?;
333            }
334        }
335
336        Ok(())
337    }
338
339    /// Worker thread main loop
340    fn worker_loop(
341        id: usize,
342        receiver: Arc<Mutex<Receiver<WorkItem>>>,
343        stats: Arc<Mutex<ThreadPoolStats>>,
344        shutdown: Arc<Mutex<bool>>,
345        worker_type: WorkType,
346    ) {
347        loop {
348            // Check for shutdown
349            if *shutdown.lock().expect("Operation failed") {
350                break;
351            }
352
353            // Try to receive work
354            let work_item = {
355                let receiver = receiver.lock().expect("Operation failed");
356                receiver.recv_timeout(Duration::from_millis(100))
357            };
358
359            match work_item {
360                Ok(item) => {
361                    let start_time = Instant::now();
362
363                    // Execute the task
364                    let result = (item.task)();
365
366                    let execution_time = start_time.elapsed().as_millis() as f64;
367
368                    // Update statistics
369                    {
370                        let mut stats_guard = stats.lock().expect("Operation failed");
371                        match result {
372                            Ok(_) => {
373                                stats_guard.tasks_completed += 1;
374                            }
375                            Err(_) => {
376                                stats_guard.tasks_failed += 1;
377                            }
378                        }
379                        stats_guard.total_execution_time_ms += execution_time;
380
381                        // Update average execution time
382                        let total_tasks = stats_guard.tasks_completed + stats_guard.tasks_failed;
383                        if total_tasks > 0 {
384                            stats_guard.avg_execution_time_ms =
385                                stats_guard.total_execution_time_ms / total_tasks as f64;
386                        }
387                    }
388                }
389                Err(mpsc::RecvTimeoutError::Timeout) => {
390                    // No work available, continue loop
391                    continue;
392                }
393                Err(mpsc::RecvTimeoutError::Disconnected) => {
394                    // Channel closed, exit worker
395                    break;
396                }
397            }
398        }
399
400        println!("Worker {id} ({worker_type:?}) shutting down");
401    }
402}
403
404/// Global thread pool instance for convenience
405static GLOBAL_THREAD_POOL: std::sync::OnceLock<ThreadPool> = std::sync::OnceLock::new();
406
407/// Initialize the global thread pool
408#[allow(dead_code)]
409pub fn init_global_thread_pool(config: ThreadPoolConfig) {
410    let _ = GLOBAL_THREAD_POOL.set(ThreadPool::new(config));
411}
412
413/// Get a reference to the global thread pool
414#[allow(dead_code)]
415pub fn global_thread_pool() -> &'static ThreadPool {
416    GLOBAL_THREAD_POOL.get_or_init(|| ThreadPool::new(ThreadPoolConfig::default()))
417}
418
419/// Execute a task on the global thread pool
420#[allow(dead_code)]
421pub fn execute<F>(work_type: WorkType, task: F) -> Result<()>
422where
423    F: FnOnce() -> Result<()> + Send + 'static,
424{
425    global_thread_pool().submit(work_type, task)
426}
427
428/// Utility function to determine optimal thread pool configuration based on system
429#[allow(dead_code)]
430pub fn optimal_config() -> ThreadPoolConfig {
431    let available_cores = thread::available_parallelism()
432        .map(|n| n.get())
433        .unwrap_or(4);
434
435    // Heuristics based on system characteristics
436    let io_threads = if available_cores <= 2 {
437        1
438    } else if available_cores <= 4 {
439        2
440    } else {
441        available_cores / 2
442    };
443
444    let cpu_threads = available_cores - io_threads;
445
446    ThreadPoolConfig {
447        io_threads,
448        cpu_threads,
449        max_queue_size: available_cores * 100, // Scale queue with cores
450        keep_alive: Duration::from_secs(30),
451        work_stealing: available_cores > 2,
452    }
453}
454
455#[cfg(test)]
456mod tests {
457    use super::*;
458    use std::sync::atomic::{AtomicUsize, Ordering};
459
460    #[test]
461    fn test_thread_pool_creation() {
462        let config = ThreadPoolConfig::default();
463        let pool = ThreadPool::new(config.clone());
464
465        assert_eq!(pool.io_workers.len(), config.io_threads);
466        assert_eq!(pool.cpu_workers.len(), config.cpu_threads);
467    }
468
469    #[test]
470    fn test_task_submission() {
471        let pool = ThreadPool::new(ThreadPoolConfig::default());
472        let counter = Arc::new(AtomicUsize::new(0));
473        let counter_clone = Arc::clone(&counter);
474
475        let result = pool.submit(WorkType::CPU, move || {
476            counter_clone.fetch_add(1, Ordering::SeqCst);
477            Ok(())
478        });
479
480        assert!(result.is_ok());
481
482        // Wait a bit for task execution
483        thread::sleep(Duration::from_millis(100));
484
485        let stats = pool.get_stats();
486        assert!(stats.tasks_submitted > 0);
487    }
488
489    #[test]
490    fn test_batch_submission() {
491        let pool = ThreadPool::new(ThreadPoolConfig::default());
492        let counter = Arc::new(AtomicUsize::new(0));
493
494        let tasks: Vec<_> = (0..10)
495            .map(|_| {
496                let counter_clone = Arc::clone(&counter);
497                move || {
498                    counter_clone.fetch_add(1, Ordering::SeqCst);
499                    Ok(())
500                }
501            })
502            .collect();
503
504        let result = pool.submit_batch(WorkType::CPU, tasks);
505        assert!(result.is_ok());
506
507        // Wait for tasks to complete
508        thread::sleep(Duration::from_millis(200));
509
510        let stats = pool.get_stats();
511        assert_eq!(stats.tasks_submitted, 10);
512    }
513
514    #[test]
515    fn test_optimal_config() {
516        let config = optimal_config();
517        assert!(config.io_threads > 0);
518        assert!(config.cpu_threads > 0);
519        assert!(config.max_queue_size > 0);
520    }
521
522    #[test]
523    fn test_global_thread_pool() {
524        // Test that global thread pool can be initialized and used
525        let result = execute(WorkType::CPU, || {
526            println!("Global thread pool test");
527            Ok(())
528        });
529
530        assert!(result.is_ok());
531    }
532}