Skip to main content

radiate_core/domain/sync/
thread_pool.rs

1use crossbeam::channel;
2use std::thread;
3use std::{
4    fmt::Debug,
5    sync::{Arc, OnceLock},
6};
7
8/// A fixed-size thread pool implementation. This thread pool will create a fixed number of worker threads
9/// that will be reused for executing jobs. This is useful for limiting the number of concurrent threads
10/// in the application.
11///
12/// The thread pool within the `FixedThreadPool` is created only once and will be reused for the lifetime of the program.
13/// Meaning that the first time you request a thread pool with a specific number of workers, that number will be used.
14/// Subsequent requests with different numbers will be ignored.
15struct FixedThreadPool {
16    inner: Arc<ThreadPool>,
17}
18
19impl FixedThreadPool {
20    /// Returns the global instance of the threadpool.
21    ///
22    /// This thread pool is fixed in size and will be created only once. This means that
23    /// the first time you call this method with a specific number of workers, that number will be used
24    /// for the lifetime of the program. Subsequent calls with different numbers will be ignored.
25    pub(self) fn instance(num_workers: usize) -> &'static FixedThreadPool {
26        static INSTANCE: OnceLock<FixedThreadPool> = OnceLock::new();
27
28        INSTANCE.get_or_init(|| FixedThreadPool {
29            inner: Arc::new(ThreadPool::new(num_workers)),
30        })
31    }
32}
33
34pub fn get_thread_pool(num_workers: usize) -> Arc<ThreadPool> {
35    Arc::clone(&FixedThreadPool::instance(num_workers).inner)
36}
37
38/// [WorkResult] is a simple wrapper around a `Receiver` that allows the user to get
39/// the result of a job that was executed in the thread pool. It kinda acts like
40/// a `Future` in a synchronous way.
41pub struct WorkResult<T> {
42    receiver: channel::Receiver<T>,
43}
44
45impl<T> WorkResult<T> {
46    pub fn new(rx: channel::Receiver<T>) -> Self {
47        WorkResult { receiver: rx }
48    }
49    /// Get the result of the job.
50    /// **Note**: This method will block until the result is available.
51    pub fn result(&self) -> T {
52        self.receiver.recv().unwrap()
53    }
54}
55
56pub struct ThreadPool {
57    sender: channel::Sender<Message>,
58    workers: Vec<Worker>,
59}
60
61impl ThreadPool {
62    /// Basic thread pool implementation.
63    ///
64    /// Create a new ThreadPool with the given size.
65    pub fn new(size: usize) -> Self {
66        let (sender, receiver) = channel::unbounded();
67
68        ThreadPool {
69            sender,
70            workers: (0..size)
71                .map(|id| Worker::new(id, receiver.clone()))
72                .collect(),
73        }
74    }
75
76    pub fn num_workers(&self) -> usize {
77        self.workers.len()
78    }
79
80    pub fn is_alive(&self) -> bool {
81        self.workers.iter().any(|worker| worker.is_alive())
82    }
83
84    /// Execute a job in the thread pool. This method does not return anything
85    /// and as such can be thought of as a 'fire-and-forget' job submission.
86    ///
87    /// # Example
88    /// ```rust,ignore
89    /// use radiate_core::domain::thread_pool::ThreadPool;
90    /// use std::sync::{Arc, Mutex};
91    ///
92    /// let pool = ThreadPool::new(4);
93    /// let counter = Arc::new(Mutex::new(0));
94    ///
95    /// for _ in 0..8 {
96    ///     let counter = Arc::clone(&counter);
97    ///     pool.submit(move || {
98    ///         let mut num = counter.lock().unwrap();
99    ///         *num += 1;
100    ///     });
101    /// }
102    ///
103    /// // Drop the pool to join all threads
104    /// drop(pool);
105    ///
106    /// assert_eq!(*counter.lock().unwrap(), 8);
107    /// ```
108    pub fn submit<F>(&self, f: F)
109    where
110        F: FnOnce() + Send + 'static,
111    {
112        let job = Box::new(f);
113        self.sender.send(Message::Work(job)).unwrap();
114    }
115
116    /// Execute a job in the thread pool and return a [WorkResult]
117    /// that can be used to get the result of the job. This method
118    /// is similar to a 'future' in that it allows the user to get
119    /// the result of the job at a later time. It should be noted that the [WorkResult]
120    /// will block when calling `result()` until the job is complete.
121    ///
122    /// # Example
123    /// ```rust,ignore
124    /// use radiate_core::domain::thread_pool::ThreadPool;
125    ///
126    /// let pool = ThreadPool::new(4);
127    /// let work_result = pool.submit_with_result(|| 10 + 32);
128    ///
129    /// // Drop the pool to join all threads
130    /// drop(pool);
131    ///
132    /// let result = work_result.result();
133    /// assert_eq!(result, 42);
134    /// ```
135    pub fn submit_with_result<F, T>(&self, f: F) -> WorkResult<T>
136    where
137        F: FnOnce() -> T + Send + 'static,
138        T: Send + 'static,
139    {
140        let (tx, rx) = channel::bounded(1);
141        let job = Box::new(move || tx.send(f()).unwrap());
142
143        self.sender.send(Message::Work(job)).unwrap();
144
145        WorkResult { receiver: rx }
146    }
147}
148
149/// Drop implementation for ThreadPool. This will terminate all workers when the ThreadPool is dropped.
150/// We need to make sure that all workers are terminated before the ThreadPool is dropped.
151impl Drop for ThreadPool {
152    fn drop(&mut self) {
153        for _ in self.workers.iter() {
154            self.sender.send(Message::Terminate).unwrap();
155        }
156
157        for worker in self.workers.iter_mut() {
158            if let Some(thread) = worker.thread.take() {
159                thread.join().unwrap();
160            }
161        }
162
163        assert!(!self.is_alive());
164    }
165}
166
167/// Job type that can be executed in the thread pool.
168type Job = Box<dyn FnOnce() + Send + 'static>;
169
170/// Message type that can be sent to the worker threads.
171enum Message {
172    Work(Job),
173    Terminate,
174}
175
176/// Worker struct that listens for incoming `Message`s and executes the `Job`s or terminates.
177struct Worker {
178    id: usize,
179    thread: Option<thread::JoinHandle<()>>,
180}
181
182impl Worker {
183    /// Create a new Worker.
184    ///
185    /// Runs jobs on a long-lived worker thread that pulls tasks from the queue.
186    fn new(id: usize, receiver: channel::Receiver<Message>) -> Self {
187        Worker {
188            id,
189            thread: Some(thread::spawn(move || {
190                loop {
191                    while let Ok(message) = receiver.recv() {
192                        match message {
193                            Message::Work(job) => job(),
194                            Message::Terminate => return,
195                        }
196                    }
197                }
198            })),
199        }
200    }
201
202    /// Simple check if the worker is alive. The thread is 'taken' when the worker is dropped.
203    /// So if the thread is 'None' the worker is no longer alive.
204    pub fn is_alive(&self) -> bool {
205        self.thread.is_some()
206    }
207}
208
209impl Debug for Worker {
210    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
211        f.debug_struct("Worker")
212            .field("id", &self.id)
213            .field("is_alive", &self.is_alive())
214            .finish()
215    }
216}
217
218#[cfg(test)]
219mod tests {
220    use super::*;
221    use crate::WaitGroup;
222    use std::{
223        sync::{Mutex, mpsc},
224        time::{Duration, Instant},
225    };
226
227    #[test]
228    fn test_thread_pool_creation() {
229        let pool = ThreadPool::new(4);
230        assert!(pool.is_alive());
231    }
232
233    #[test]
234    fn test_basic_job_execution() {
235        let pool = ThreadPool::new(4);
236        let counter = Arc::new(Mutex::new(0));
237
238        for _ in 0..8 {
239            let counter = Arc::clone(&counter);
240            pool.submit(move || {
241                let mut num = counter.lock().unwrap();
242                *num += 1;
243            });
244        }
245
246        // Give threads some time to finish processing
247        thread::sleep(Duration::from_secs(1));
248        assert_eq!(*counter.lock().unwrap(), 8);
249    }
250
251    #[test]
252    fn test_thread_pool() {
253        let pool = ThreadPool::new(4);
254
255        for i in 0..8 {
256            pool.submit(move || {
257                let start_time = std::time::SystemTime::now();
258                println!("Job {} started.", i);
259                thread::sleep(Duration::from_secs(1));
260                println!("Job {} finished in {:?}.", i, start_time.elapsed().unwrap());
261            });
262        }
263    }
264
265    #[test]
266    fn test_job_order() {
267        let pool = ThreadPool::new(2);
268        let results = Arc::new(Mutex::new(vec![]));
269
270        for i in 0..5 {
271            let results = Arc::clone(&results);
272            pool.submit(move || {
273                results.lock().unwrap().push(i);
274            });
275        }
276
277        // Give threads some time to finish processing
278        thread::sleep(Duration::from_secs(1));
279        let mut results = results.lock().unwrap();
280        results.sort(); // Order may not be guaranteed
281        assert_eq!(*results, vec![0, 1, 2, 3, 4]);
282    }
283
284    #[test]
285    fn test_thread_pool_process() {
286        let pool = ThreadPool::new(4);
287
288        let results = pool.submit_with_result(|| {
289            let start_time = std::time::SystemTime::now();
290            println!("Job started.");
291            thread::sleep(Duration::from_secs(2));
292            println!("Job finished in {:?}.", start_time.elapsed().unwrap());
293            42
294        });
295
296        let result = results.result();
297        assert_eq!(result, 42);
298    }
299
300    #[test]
301    fn test_max_concurrent_jobs() {
302        let pool = ThreadPool::new(4);
303        let (tx, rx) = mpsc::channel();
304        let num_jobs = 20;
305        let start_time = Instant::now();
306
307        // Submit 20 jobs
308        for i in 0..num_jobs {
309            let tx = tx.clone();
310            pool.submit(move || {
311                thread::sleep(Duration::from_millis(100));
312                tx.send(i).unwrap();
313            });
314        }
315
316        // Wait for all jobs to finish
317        let mut results = vec![];
318        for _ in 0..num_jobs {
319            results.push(rx.recv().unwrap());
320        }
321
322        let elapsed = start_time.elapsed();
323        assert!(elapsed < Duration::from_secs(3));
324        assert_eq!(results.len(), num_jobs);
325        assert!(results.iter().all(|&x| x < num_jobs));
326    }
327
328    #[test]
329    fn tests_thread_pool_submit_with_result_returns_correct_order() {
330        let pool = ThreadPool::new(5);
331        let num_jobs = 10;
332        let mut work_results = vec![];
333
334        for i in 0..num_jobs {
335            let work_result = pool.submit_with_result(move || {
336                thread::sleep(Duration::from_millis(50 * (num_jobs - i) as u64));
337                i * i
338            });
339            work_results.push(work_result);
340        }
341
342        for (i, work_result) in work_results.into_iter().enumerate() {
343            let result = work_result.result();
344            assert_eq!(result, i * i);
345        }
346    }
347
348    #[test]
349    fn test_wait_group() {
350        let pool = ThreadPool::new(4);
351        let wg = WaitGroup::new();
352        let num_tasks = 10;
353        let total = Arc::new(Mutex::new(0));
354
355        for _ in 0..num_tasks {
356            let guard = wg.guard();
357            let total = Arc::clone(&total);
358            pool.submit(move || {
359                thread::sleep(Duration::from_millis(100));
360                let mut num = total.lock().unwrap();
361                *num += 1;
362                drop(guard);
363            });
364        }
365
366        // Not all tasks should be done yet - so the total should be less than num_tasks
367        {
368            let total = total.lock().unwrap();
369            assert_ne!(*total, num_tasks);
370        }
371
372        let total_tasks_waited_for = wg.wait();
373
374        // Now all tasks should be done - so the total should equal num_tasks
375        let total = total.lock().unwrap();
376        assert_eq!(*total, num_tasks);
377        assert_eq!(total_tasks_waited_for, num_tasks);
378    }
379
380    #[test]
381    fn test_wait_group_zero_tasks() {
382        let wg = WaitGroup::new();
383        let total_tasks_waited_for = wg.wait();
384        assert_eq!(total_tasks_waited_for, 0);
385    }
386}