Skip to main content

ntex_rt/
pool.rs

1//! A thread pool for blocking operations.
2use std::sync::{Arc, atomic::AtomicUsize, atomic::Ordering};
3use std::task::{Context, Poll};
4use std::{any::Any, fmt, future::Future, panic, pin::Pin, thread, time::Duration};
5
6use crossbeam_channel::{Receiver, Select, Sender, TrySendError, bounded, unbounded};
7
8/// Spawns a blocking task on a new thread and waits for it to complete.
9///
10/// If the returned future is dropped, the blocking task is cancelled.
11/// Call `detach` to allow the task to continue running in the background.
12pub fn spawn_blocking<F, R>(f: F) -> BlockingResult<R>
13where
14    F: FnOnce() -> R + Send + 'static,
15    R: Send + 'static,
16{
17    if let Some(sys) = crate::System::try_current() {
18        sys.spawn_blocking(f)
19    } else {
20        ThreadPool::execute_inplace(f)
21    }
22}
23
24/// An error that may be emitted when all worker threads are busy.
25#[derive(Copy, Clone, Debug, PartialEq, Eq)]
26pub struct BlockingError;
27
28impl std::error::Error for BlockingError {}
29
30impl fmt::Display for BlockingError {
31    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
32        "All threads are busy".fmt(f)
33    }
34}
35
36#[derive(Debug)]
37pub struct BlockingResult<T> {
38    rx: oneshot::AsyncReceiver<Result<T, Box<dyn Any + Send>>>,
39}
40
41impl<T: 'static> BlockingResult<T> {
42    /// Detaches the task to let it keep running in the background
43    pub fn detach(self) {
44        crate::spawn(async move {
45            let _ = self.await;
46        })
47        .detach();
48    }
49}
50
51type BoxedDispatchable = Box<dyn Dispatchable + Send>;
52
53pub(crate) trait Dispatchable: Send + 'static {
54    fn run(self: Box<Self>);
55}
56
57impl<F> Dispatchable for F
58where
59    F: FnOnce() + Send + 'static,
60{
61    fn run(self: Box<Self>) {
62        (*self)();
63    }
64}
65
66struct CounterGuard(Arc<AtomicUsize>);
67
68impl Drop for CounterGuard {
69    fn drop(&mut self) {
70        self.0.fetch_sub(1, Ordering::AcqRel);
71    }
72}
73
74fn worker(
75    receiver_high_prio: Receiver<BoxedDispatchable>,
76    receiver_low_prio: Receiver<BoxedDispatchable>,
77    counter: Arc<AtomicUsize>,
78    timeout: Duration,
79) -> impl FnOnce() {
80    move || {
81        counter.fetch_add(1, Ordering::AcqRel);
82        let _guard = CounterGuard(counter);
83        let mut sel = Select::new_biased();
84        sel.recv(&receiver_high_prio);
85        sel.recv(&receiver_low_prio);
86        while let Ok(op) = sel.select_timeout(timeout) {
87            match op {
88                op if op.index() == 0 => {
89                    if let Ok(f) = op.recv(&receiver_high_prio) {
90                        f.run();
91                    }
92                }
93                op if op.index() == 1 => {
94                    if let Ok(f) = op.recv(&receiver_low_prio) {
95                        f.run();
96                    }
97                }
98                _ => unreachable!(),
99            }
100        }
101    }
102}
103
104/// A thread pool for executing blocking operations.
105///
106/// The pool can be configured as either bounded or unbounded, which
107/// determines how tasks are handled when all worker threads are busy.
108///
109/// - In a **bounded** pool, submitting a task will fail if the number of
110///   concurrent operations has reached the thread limit.
111/// - In an **unbounded** pool, tasks are queued and will wait until a
112///   worker thread becomes available.
113///
114/// The number of worker threads scales dynamically with load, but will
115/// never exceed the `thread_limit` parameter.
116#[derive(Debug, Clone)]
117pub struct ThreadPool {
118    name: String,
119    sender_low_prio: Sender<BoxedDispatchable>,
120    receiver_low_prio: Receiver<BoxedDispatchable>,
121    sender_high_prio: Sender<BoxedDispatchable>,
122    receiver_high_prio: Receiver<BoxedDispatchable>,
123    counter: Arc<AtomicUsize>,
124    thread_limit: usize,
125    recv_timeout: Duration,
126}
127
128impl ThreadPool {
129    /// Creates a [`ThreadPool`] with a maximum number of worker threads
130    /// and a timeout for receiving tasks from the task channel.
131    pub fn new(name: &str, thread_limit: usize, recv_timeout: Duration) -> Self {
132        let (sender_low_prio, receiver_low_prio) = bounded(0);
133        let (sender_high_prio, receiver_high_prio) = unbounded();
134        Self {
135            sender_low_prio,
136            receiver_low_prio,
137            sender_high_prio,
138            receiver_high_prio,
139            thread_limit,
140            recv_timeout,
141            name: format!("{name}:pool-wrk"),
142            counter: Arc::new(AtomicUsize::new(0)),
143        }
144    }
145
146    pub(crate) fn execute_inplace<F, R>(f: F) -> BlockingResult<R>
147    where
148        F: FnOnce() -> R + Send + 'static,
149        R: Send + 'static,
150    {
151        let (tx, rx) = oneshot::async_channel();
152        let result = panic::catch_unwind(panic::AssertUnwindSafe(f));
153        let _ = tx.send(result);
154        BlockingResult { rx }
155    }
156
157    #[allow(clippy::missing_panics_doc)]
158    /// Submits a task (closure) to the thread pool.
159    ///
160    /// The task will be executed by an available worker thread.
161    /// If no threads are available and the pool has reached its maximum size,
162    /// the work will be queued until a worker thread becomes available.
163    pub fn execute<F, R>(&self, f: F) -> BlockingResult<R>
164    where
165        F: FnOnce() -> R + Send + 'static,
166        R: Send + 'static,
167    {
168        let (tx, rx) = oneshot::async_channel();
169        let f = Box::new(move || {
170            // do not execute operation if receiver is dropped
171            if !tx.is_closed() {
172                let result = panic::catch_unwind(panic::AssertUnwindSafe(f));
173                let _ = tx.send(result);
174            }
175        });
176
177        match self.sender_low_prio.try_send(f) {
178            Ok(()) => BlockingResult { rx },
179            Err(e) => match e {
180                TrySendError::Full(f) => {
181                    let cnt = self.counter.load(Ordering::Acquire);
182                    if cnt >= self.thread_limit {
183                        self.sender_high_prio
184                            .send(f)
185                            .expect("the channel should not be full");
186                        BlockingResult { rx }
187                    } else {
188                        thread::Builder::new()
189                            .name(format!("{}:{}", self.name, cnt))
190                            .spawn(worker(
191                                self.receiver_high_prio.clone(),
192                                self.receiver_low_prio.clone(),
193                                self.counter.clone(),
194                                self.recv_timeout,
195                            ))
196                            .expect("Cannot construct new thread");
197                        self.sender_low_prio
198                            .send(f)
199                            .expect("the channel should not be full");
200                        BlockingResult { rx }
201                    }
202                }
203                TrySendError::Disconnected(_) => {
204                    unreachable!("receiver should not all disconnected")
205                }
206            },
207        }
208    }
209}
210
211impl<R> Future for BlockingResult<R> {
212    type Output = Result<R, BlockingError>;
213
214    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
215        let this = self.get_mut();
216
217        match Pin::new(&mut this.rx).poll(cx) {
218            Poll::Pending => Poll::Pending,
219            Poll::Ready(result) => Poll::Ready(
220                result
221                    .map_err(|_| BlockingError)
222                    .and_then(|res| res.map_err(|_| BlockingError)),
223            ),
224        }
225    }
226}