Skip to main content

ntex_rt/
pool.rs

1//! A thread pool for blocking operations.
2use std::sync::Arc;
3use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering, fence};
4use std::task::{Context, Poll};
5use std::time::{Duration, Instant};
6use std::{any::Any, fmt, future::Future, panic, pin::Pin, thread};
7
8use crossbeam_channel::{Receiver, Select, Sender, TrySendError, bounded, unbounded};
9
10/// Submits blocking work and returns a future for its result.
11///
12/// If a system is running, work is submitted to its blocking thread pool.
13/// Otherwise, the closure runs immediately on the current thread.
14///
15/// Dropping the returned future prevents queued work from starting, but cannot
16/// interrupt work that is already running. Call [`BlockingResult::detach`] to
17/// let queued work continue even if its result is no longer needed.
18pub fn spawn_blocking<F, R>(f: F) -> BlockingResult<R>
19where
20    F: FnOnce() -> R + Send + 'static,
21    R: Send + 'static,
22{
23    if let Some(sys) = crate::System::try_current() {
24        sys.spawn_blocking(f)
25    } else {
26        ThreadPool::execute_inplace(f)
27    }
28}
29
30/// Error returned when blocking work cannot produce a result.
31///
32/// This can occur if the task is canceled, panics, or no worker thread
33/// can be started.
34#[derive(Copy, Clone, Debug, PartialEq, Eq)]
35pub struct BlockingError;
36
37impl std::error::Error for BlockingError {}
38
39impl fmt::Display for BlockingError {
40    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
41        "Blocking task failed or was canceled".fmt(f)
42    }
43}
44
45/// Future resolving to the result of blocking work.
46#[derive(Debug)]
47pub struct BlockingResult<T> {
48    rx: oneshot::AsyncReceiver<Result<T, Box<dyn Any + Send>>>,
49    detached: Option<Arc<AtomicBool>>,
50}
51
52impl<T> BlockingResult<T> {
53    /// Detaches the task so it can continue without awaiting its result.
54    ///
55    /// Does not require a running runtime.
56    pub fn detach(self) {
57        if let Some(detached) = &self.detached {
58            // published by dropping the receiver
59            detached.store(true, Ordering::Relaxed);
60        }
61    }
62}
63
64/// Snapshot of [`ThreadPool`] metrics.
65///
66/// Durations and task counts are totals since the pool was created. Rates
67/// and averages over an interval are computed from the deltas of two
68/// snapshots, e.g. the average queue wait is the `wait_duration` delta
69/// divided by the `tasks` delta.
70#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
71#[non_exhaustive]
72pub struct PoolMetrics {
73    /// Maximum number of worker threads.
74    pub thread_limit: usize,
75    /// Number of worker threads, busy or idle.
76    pub threads: usize,
77    /// Number of tasks being executed.
78    pub active_tasks: usize,
79    /// Number of tasks waiting for a worker thread.
80    pub queue_depth: usize,
81    /// Number of tasks started.
82    pub tasks: u64,
83    /// Total time started tasks waited for a worker thread.
84    pub wait_duration: Duration,
85    /// Total time worker threads spent executing tasks.
86    pub busy_duration: Duration,
87}
88
89#[derive(Debug, Default)]
90struct PoolStats {
91    active: AtomicUsize,
92    tasks: AtomicU64,
93    // nanoseconds
94    wait: AtomicU64,
95    busy: AtomicU64,
96}
97
98fn as_nanos(d: Duration) -> u64 {
99    u64::try_from(d.as_nanos()).unwrap_or(u64::MAX)
100}
101
102type BoxedDispatchable = Box<dyn Dispatchable + Send>;
103
104pub(crate) trait Dispatchable: Send + 'static {
105    fn run(self: Box<Self>);
106}
107
108impl<F> Dispatchable for F
109where
110    F: FnOnce() + Send + 'static,
111{
112    fn run(self: Box<Self>) {
113        (*self)();
114    }
115}
116
117/// Reserved worker slot, released on drop.
118struct CounterGuard(Arc<AtomicUsize>);
119
120impl CounterGuard {
121    fn reserve(counter: &Arc<AtomicUsize>, limit: usize) -> Option<(Self, usize)> {
122        counter
123            .try_update(Ordering::AcqRel, Ordering::Acquire, |cnt| {
124                (cnt < limit).then_some(cnt + 1)
125            })
126            .ok()
127            .map(|cnt| (CounterGuard(counter.clone()), cnt))
128    }
129}
130
131impl Drop for CounterGuard {
132    fn drop(&mut self) {
133        self.0.fetch_sub(1, Ordering::AcqRel);
134    }
135}
136
137/// Runs a task, keeping the worker alive if result delivery or disposal panics.
138fn run_task(f: BoxedDispatchable) {
139    if let Err(payload) = panic::catch_unwind(panic::AssertUnwindSafe(|| f.run())) {
140        log::error!("Blocking pool task panicked during completion");
141        // the payload destructor may panic as well
142        std::mem::forget(payload);
143    }
144}
145
146fn worker(
147    receiver_high_prio: Receiver<BoxedDispatchable>,
148    receiver_low_prio: Receiver<BoxedDispatchable>,
149    guard: CounterGuard,
150    thread_limit: usize,
151    timeout: Duration,
152) -> impl FnOnce() {
153    move || {
154        let mut guard = guard;
155        let mut sel = Select::new_biased();
156        sel.recv(&receiver_high_prio);
157        sel.recv(&receiver_low_prio);
158        loop {
159            let task = match sel.select_timeout(timeout) {
160                Ok(op) if op.index() == 0 => op.recv(&receiver_high_prio),
161                Ok(op) => op.recv(&receiver_low_prio),
162                Err(_) => {
163                    // release the slot, then pick up work queued meanwhile,
164                    // pairs with the fence in `ThreadPool::execute`
165                    let counter = guard.0.clone();
166                    drop(guard);
167                    fence(Ordering::SeqCst);
168                    if receiver_high_prio.is_empty() {
169                        return;
170                    }
171                    match CounterGuard::reserve(&counter, thread_limit) {
172                        Some((g, _)) => guard = g,
173                        None => return,
174                    }
175                    continue;
176                }
177            };
178            if let Ok(f) = task {
179                run_task(f);
180            } else {
181                // the pool is dropped and the channels stay selectable,
182                // run the queued work and exit
183                while let Ok(f) = receiver_high_prio.try_recv() {
184                    run_task(f);
185                }
186                return;
187            }
188        }
189    }
190}
191
192/// A thread pool for executing blocking operations.
193///
194/// The pool can be configured as either bounded or unbounded, which
195/// determines how tasks are handled when all worker threads are busy.
196///
197/// The number of worker threads scales dynamically with load, but will
198/// never exceed the `thread_limit` parameter. When all worker threads are
199/// busy, tasks are queued until a worker thread becomes available.
200#[derive(Debug, Clone)]
201pub struct ThreadPool {
202    name: String,
203    sender_low_prio: Sender<BoxedDispatchable>,
204    receiver_low_prio: Receiver<BoxedDispatchable>,
205    sender_high_prio: Sender<BoxedDispatchable>,
206    receiver_high_prio: Receiver<BoxedDispatchable>,
207    counter: Arc<AtomicUsize>,
208    stats: Arc<PoolStats>,
209    thread_limit: usize,
210    recv_timeout: Duration,
211}
212
213impl ThreadPool {
214    /// Creates a [`ThreadPool`] with a maximum number of worker threads
215    /// and a timeout for receiving tasks from the task channel.
216    ///
217    /// A `thread_limit` of zero is treated as one.
218    pub fn new(name: &str, thread_limit: usize, recv_timeout: Duration) -> Self {
219        let (sender_low_prio, receiver_low_prio) = bounded(0);
220        let (sender_high_prio, receiver_high_prio) = unbounded();
221        Self {
222            sender_low_prio,
223            receiver_low_prio,
224            sender_high_prio,
225            receiver_high_prio,
226            thread_limit: thread_limit.max(1),
227            recv_timeout,
228            name: format!("{name}:pool-wrk"),
229            counter: Arc::new(AtomicUsize::new(0)),
230            stats: Arc::default(),
231        }
232    }
233
234    /// Returns a snapshot of the pool metrics.
235    ///
236    /// Gauges and totals are read independently and may be slightly
237    /// inconsistent with each other.
238    pub fn metrics(&self) -> PoolMetrics {
239        PoolMetrics {
240            thread_limit: self.thread_limit,
241            threads: self.counter.load(Ordering::Relaxed),
242            active_tasks: self.stats.active.load(Ordering::Relaxed),
243            queue_depth: self.receiver_high_prio.len(),
244            tasks: self.stats.tasks.load(Ordering::Relaxed),
245            wait_duration: Duration::from_nanos(self.stats.wait.load(Ordering::Relaxed)),
246            busy_duration: Duration::from_nanos(self.stats.busy.load(Ordering::Relaxed)),
247        }
248    }
249
250    pub(crate) fn execute_inplace<F, R>(f: F) -> BlockingResult<R>
251    where
252        F: FnOnce() -> R + Send + 'static,
253        R: Send + 'static,
254    {
255        let (tx, rx) = oneshot::async_channel();
256        let result = panic::catch_unwind(panic::AssertUnwindSafe(f));
257        let _ = tx.send(result);
258        BlockingResult { rx, detached: None }
259    }
260
261    #[allow(clippy::missing_panics_doc)]
262    /// Submits a closure to the thread pool.
263    ///
264    /// The task will be executed by an available worker thread. If no threads
265    /// are available and the pool has reached its maximum size, the work will
266    /// be queued until a worker thread becomes available. This method never
267    /// blocks.
268    pub fn execute<F, R>(&self, f: F) -> BlockingResult<R>
269    where
270        F: FnOnce() -> R + Send + 'static,
271        R: Send + 'static,
272    {
273        let (tx, rx) = oneshot::async_channel();
274        let detached = Arc::new(AtomicBool::new(false));
275        let res = BlockingResult {
276            rx,
277            detached: Some(detached.clone()),
278        };
279        let stats = self.stats.clone();
280        let queued = Instant::now();
281        let f = Box::new(move || {
282            // do not execute operation if receiver is dropped, unless detached,
283            // the fence pairs with the receiver drop in `detach`
284            if !tx.is_closed() || {
285                fence(Ordering::Acquire);
286                detached.load(Ordering::Relaxed)
287            } {
288                let start = Instant::now();
289                stats.tasks.fetch_add(1, Ordering::Relaxed);
290                stats
291                    .wait
292                    .fetch_add(as_nanos(start - queued), Ordering::Relaxed);
293                stats.active.fetch_add(1, Ordering::Relaxed);
294
295                let result = panic::catch_unwind(panic::AssertUnwindSafe(f));
296
297                stats.active.fetch_sub(1, Ordering::Relaxed);
298                stats
299                    .busy
300                    .fetch_add(as_nanos(start.elapsed()), Ordering::Relaxed);
301                let _ = tx.send(result);
302            }
303        });
304
305        // hand over to an idle worker
306        let f = match self.sender_low_prio.try_send(f) {
307            Ok(()) => return res,
308            Err(TrySendError::Full(f)) => f,
309            Err(TrySendError::Disconnected(_)) => {
310                unreachable!("receiver should not all disconnected")
311            }
312        };
313
314        self.sender_high_prio
315            .send(f)
316            .expect("the channel should not be closed");
317        // pairs with the fence in `worker`, either an exiting worker sees
318        // the queued task or a free slot is visible here
319        fence(Ordering::SeqCst);
320
321        if let Some((guard, idx)) = CounterGuard::reserve(&self.counter, self.thread_limit) {
322            let result = thread::Builder::new()
323                .name(format!("{}:{}", self.name, idx))
324                .spawn(worker(
325                    self.receiver_high_prio.clone(),
326                    self.receiver_low_prio.clone(),
327                    guard,
328                    self.thread_limit,
329                    self.recv_timeout,
330                ));
331            if let Err(e) = result {
332                log::error!("Cannot start blocking pool thread: {e}");
333                // no worker can run queued tasks, drop them so they
334                // resolve with `BlockingError`
335                while self.counter.load(Ordering::Acquire) == 0 {
336                    if self.receiver_high_prio.try_recv().is_err() {
337                        break;
338                    }
339                }
340            }
341        }
342        res
343    }
344}
345
346impl<R> Future for BlockingResult<R> {
347    type Output = Result<R, BlockingError>;
348
349    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
350        let this = self.get_mut();
351
352        match Pin::new(&mut this.rx).poll(cx) {
353            Poll::Pending => Poll::Pending,
354            Poll::Ready(result) => Poll::Ready(
355                result
356                    .map_err(|_| BlockingError)
357                    .and_then(|res| res.map_err(|_| BlockingError)),
358            ),
359        }
360    }
361}
362
363#[cfg(test)]
364mod tests {
365    use std::time::Instant;
366
367    use super::*;
368
369    fn wait<R>(fut: BlockingResult<R>, timeout: Duration) -> Option<Result<R, BlockingError>> {
370        let mut fut = std::pin::pin!(fut);
371        let mut cx = Context::from_waker(std::task::Waker::noop());
372        let start = Instant::now();
373        loop {
374            if let Poll::Ready(res) = fut.as_mut().poll(&mut cx) {
375                return Some(res);
376            }
377            if start.elapsed() > timeout {
378                return None;
379            }
380            thread::sleep(Duration::from_millis(1));
381        }
382    }
383
384    #[test]
385    fn thread_limit_respected() {
386        let pool = ThreadPool::new("test", 2, Duration::from_secs(1));
387        let running = Arc::new(AtomicUsize::new(0));
388        let max = Arc::new(AtomicUsize::new(0));
389        let barrier = Arc::new(std::sync::Barrier::new(8));
390
391        // concurrent submitters
392        let submitters: Vec<_> = (0..8)
393            .map(|_| {
394                let (pool, running, max, barrier) =
395                    (pool.clone(), running.clone(), max.clone(), barrier.clone());
396                thread::spawn(move || {
397                    barrier.wait();
398                    (0..8)
399                        .map(|_| {
400                            let (running, max) = (running.clone(), max.clone());
401                            pool.execute(move || {
402                                let cnt = running.fetch_add(1, Ordering::SeqCst) + 1;
403                                max.fetch_max(cnt, Ordering::SeqCst);
404                                thread::sleep(Duration::from_millis(5));
405                                running.fetch_sub(1, Ordering::SeqCst);
406                            })
407                        })
408                        .collect::<Vec<_>>()
409                })
410            })
411            .collect();
412        for s in submitters {
413            for res in s.join().unwrap() {
414                assert_eq!(wait(res, Duration::from_secs(10)), Some(Ok(())));
415            }
416        }
417        assert!(max.load(Ordering::SeqCst) <= 2, "{max:?} tasks ran at once");
418    }
419
420    #[test]
421    fn idle_workers_do_not_strand_tasks() {
422        let pool = ThreadPool::new("test", 1, Duration::from_millis(2));
423        for i in 0..300u64 {
424            // submit around the moment the idle worker times out
425            thread::sleep(Duration::from_micros(1500 + (i % 10) * 100));
426            let res = wait(pool.execute(move || i), Duration::from_secs(5));
427            assert_eq!(res, Some(Ok(i)), "task {i} was not executed");
428        }
429    }
430
431    #[test]
432    fn spawn_blocking_without_system() {
433        thread::spawn(|| {
434            let tid = thread::current().id();
435            let res = spawn_blocking(move || thread::current().id() == tid);
436            assert_eq!(wait(res, Duration::from_secs(1)), Some(Ok(true)));
437
438            let res = spawn_blocking(|| panic!("blocking"));
439            assert_eq!(
440                wait(res, Duration::from_secs(1)),
441                Some(Err::<(), _>(BlockingError))
442            );
443        })
444        .join()
445        .unwrap();
446        assert_eq!(
447            BlockingError.to_string(),
448            "Blocking task failed or was canceled"
449        );
450    }
451
452    #[test]
453    fn detached_blocking_task_runs() {
454        crate::System::new("test", crate::testing::TestRunner).block_on(async {
455            let (tx, rx) = oneshot::async_channel();
456            spawn_blocking(move || tx.send(1).unwrap()).detach();
457            assert_eq!(rx.await, Ok(1));
458        });
459    }
460
461    #[test]
462    fn detach_without_runtime() {
463        thread::spawn(|| spawn_blocking(|| 1).detach())
464            .join()
465            .unwrap();
466
467        // occupy the only worker, the next tasks wait in the queue
468        let pool = ThreadPool::new("test", 1, Duration::from_secs(1));
469        let (start_tx, start_rx) = std::sync::mpsc::channel();
470        let (release_tx, release_rx) = std::sync::mpsc::channel::<()>();
471        let first = pool.execute(move || {
472            start_tx.send(()).unwrap();
473            release_rx.recv().unwrap();
474        });
475        start_rx.recv().unwrap();
476
477        let dropped = Arc::new(AtomicBool::new(false));
478        let dropped2 = dropped.clone();
479        drop(pool.execute(move || dropped2.store(true, Ordering::Relaxed)));
480        let (tx, rx) = std::sync::mpsc::channel();
481        pool.execute(move || tx.send(1).unwrap()).detach();
482        release_tx.send(()).unwrap();
483
484        assert_eq!(rx.recv_timeout(Duration::from_secs(5)), Ok(1));
485        assert_eq!(wait(first, Duration::from_secs(5)), Some(Ok(())));
486        // dropped results cancel queued work
487        assert!(!dropped.load(Ordering::Relaxed));
488    }
489
490    #[test]
491    fn metrics() {
492        let pool = ThreadPool::new("test", 1, Duration::from_secs(1));
493        let m = pool.metrics();
494        assert_eq!((m.thread_limit, m.threads, m.tasks), (1, 0, 0));
495
496        // occupy the only worker, the second task waits in the queue
497        let (start_tx, start_rx) = std::sync::mpsc::channel();
498        let (release_tx, release_rx) = std::sync::mpsc::channel::<()>();
499        let first = pool.execute(move || {
500            start_tx.send(()).unwrap();
501            release_rx.recv().unwrap();
502        });
503        start_rx.recv().unwrap();
504        let second = pool.execute(|| ());
505
506        let m = pool.metrics();
507        assert_eq!((m.threads, m.active_tasks, m.queue_depth), (1, 1, 1));
508        assert_eq!(m.tasks, 1);
509
510        let sleep = Duration::from_millis(20);
511        thread::sleep(sleep);
512        release_tx.send(()).unwrap();
513        assert_eq!(wait(first, Duration::from_secs(5)), Some(Ok(())));
514        assert_eq!(wait(second, Duration::from_secs(5)), Some(Ok(())));
515
516        let m = pool.metrics();
517        assert_eq!((m.active_tasks, m.queue_depth, m.tasks), (0, 0, 2));
518        assert!(m.wait_duration >= sleep);
519        assert!(m.busy_duration >= sleep);
520    }
521
522    #[test]
523    fn dropped_pool_releases_workers() {
524        // the idle timeout is never reached, workers exit on disconnection
525        let pool = ThreadPool::new("test", 1, Duration::from_secs(60));
526        let counter = pool.counter.clone();
527        let (start_tx, start_rx) = std::sync::mpsc::channel();
528        let (release_tx, release_rx) = std::sync::mpsc::channel::<()>();
529        let first = pool.execute(move || {
530            start_tx.send(()).unwrap();
531            release_rx.recv().unwrap();
532        });
533        start_rx.recv().unwrap();
534        let queued = pool.execute(|| 1);
535        drop(pool);
536        release_tx.send(()).unwrap();
537
538        assert_eq!(wait(first, Duration::from_secs(5)), Some(Ok(())));
539        assert_eq!(wait(queued, Duration::from_secs(5)), Some(Ok(1)));
540        let start = Instant::now();
541        while counter.load(Ordering::Acquire) != 0 {
542            assert!(start.elapsed() < Duration::from_secs(5), "worker is alive");
543            thread::sleep(Duration::from_millis(1));
544        }
545    }
546
547    #[test]
548    fn result_drop_panic_keeps_worker() {
549        struct PanicOnDrop;
550        impl Drop for PanicOnDrop {
551            fn drop(&mut self) {
552                panic!("result destructor");
553            }
554        }
555
556        let pool = ThreadPool::new("test", 1, Duration::from_secs(60));
557        let (start_tx, start_rx) = std::sync::mpsc::channel();
558        let (release_tx, release_rx) = std::sync::mpsc::channel::<()>();
559        let first = pool.execute(move || {
560            start_tx.send(()).unwrap();
561            release_rx.recv().unwrap();
562            PanicOnDrop
563        });
564        start_rx.recv().unwrap();
565        let queued = pool.execute(|| 1);
566        // the unclaimed result is dropped on the worker
567        drop(first);
568        release_tx.send(()).unwrap();
569
570        assert_eq!(wait(queued, Duration::from_secs(2)), Some(Ok(1)));
571        assert_eq!(pool.metrics().threads, 1);
572    }
573
574    #[test]
575    fn zero_thread_limit() {
576        let pool = ThreadPool::new("test", 0, Duration::from_secs(1));
577        assert_eq!(
578            wait(pool.execute(|| 1), Duration::from_secs(5)),
579            Some(Ok(1))
580        );
581    }
582}