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