Skip to main content

tokio/runtime/blocking/
pool.rs

1//! Thread pool for blocking operations
2
3use crate::loom::sync::{Arc, Condvar, Mutex};
4use crate::loom::thread;
5use crate::runtime::blocking::schedule::BlockingSchedule;
6use crate::runtime::blocking::sharded::ShardedImpl;
7use crate::runtime::blocking::{shutdown, BlockingTask};
8use crate::runtime::builder::ThreadNameFn;
9use crate::runtime::task::{self, JoinHandle};
10use crate::runtime::{AutoBox, Builder, Callback, Handle};
11use crate::util::metric_atomics::MetricAtomicUsize;
12use crate::util::trace::{blocking_task, SpawnMeta};
13
14use std::collections::{HashMap, VecDeque};
15use std::fmt;
16use std::io;
17use std::sync::atomic::Ordering;
18use std::time::Duration;
19
20pub(crate) struct BlockingPool {
21    spawner: Spawner,
22    shutdown_rx: shutdown::Receiver,
23}
24
25#[derive(Clone)]
26pub(crate) struct Spawner {
27    inner: Arc<Inner>,
28}
29
30#[derive(Default)]
31pub(crate) struct SpawnerMetrics {
32    num_threads: MetricAtomicUsize,
33    num_idle_threads: MetricAtomicUsize,
34    queue_depth: MetricAtomicUsize,
35}
36
37impl SpawnerMetrics {
38    pub(super) fn num_threads(&self) -> usize {
39        self.num_threads.load(Ordering::Relaxed)
40    }
41
42    pub(super) fn num_idle_threads(&self) -> usize {
43        self.num_idle_threads.load(Ordering::Relaxed)
44    }
45
46    cfg_unstable_metrics! {
47        fn queue_depth(&self) -> usize {
48            self.queue_depth.load(Ordering::Relaxed)
49        }
50    }
51
52    fn inc_num_threads(&self) {
53        self.num_threads.increment();
54    }
55
56    pub(super) fn dec_num_threads(&self) {
57        self.num_threads.decrement();
58    }
59
60    pub(super) fn inc_num_idle_threads(&self) {
61        self.num_idle_threads.increment();
62    }
63
64    pub(super) fn dec_num_idle_threads(&self) -> usize {
65        self.num_idle_threads.decrement()
66    }
67
68    pub(super) fn inc_queue_depth(&self) {
69        self.queue_depth.increment();
70    }
71
72    pub(super) fn dec_queue_depth(&self) {
73        self.queue_depth.decrement();
74    }
75}
76
77struct Inner {
78    /// Queue + notification implementation.
79    inner_impl: InnerImpl,
80
81    /// Spawned threads use this name.
82    thread_name: ThreadNameFn,
83
84    /// Spawned thread stack size.
85    stack_size: Option<usize>,
86
87    /// Call after a thread starts.
88    after_start: Option<Callback>,
89
90    /// Call before a thread stops.
91    before_stop: Option<Callback>,
92
93    // Maximum number of threads.
94    thread_cap: usize,
95
96    // Number of runtime scheduler workers counted in `num_threads`.
97    scheduler_threads: usize,
98
99    // Customizable wait timeout.
100    keep_alive: Duration,
101
102    // Metrics about the pool.
103    metrics: SpawnerMetrics,
104}
105
106/// Per-variant queue + notification + lock topology.
107enum InnerImpl {
108    Locked(LockedImpl),
109    Sharded(ShardedImpl),
110}
111
112/// Single-mutex + condvar implementation.
113struct LockedImpl {
114    mutex: Mutex<LockedInner>,
115    condvar: Condvar,
116}
117
118struct LockedInner {
119    queue: VecDeque<Task>,
120    num_notify: u32,
121    /// Thread-management state. Split apart so that it's re-usable in a
122    /// sharded queue implementation.
123    thread_mgmt_state: ThreadManagementState,
124}
125
126/// State handed back from `InnerImpl::begin_shutdown` to the caller so it
127/// can join the worker threads after the wait completes: an optional
128/// previously-timed-out worker, plus the map of currently-running workers.
129pub(super) type ShutdownHandles = (
130    Option<thread::JoinHandle<()>>,
131    HashMap<usize, thread::JoinHandle<()>>,
132);
133
134/// Thread-management state used by every `InnerImpl` variant.
135pub(super) struct ThreadManagementState {
136    pub(super) shutdown: bool,
137    pub(super) shutdown_tx: Option<shutdown::Sender>,
138    /// Prior to shutdown, we clean up `JoinHandles` by having each timed-out
139    /// thread join on the previous timed-out thread. This is not strictly
140    /// necessary but helps avoid Valgrind false positives, see
141    /// <https://github.com/tokio-rs/tokio/commit/646fbae76535e397ef79dbcaacb945d4c829f666>
142    /// for more information.
143    pub(super) last_exiting_thread: Option<thread::JoinHandle<()>>,
144    /// This holds the `JoinHandles` for all running threads; on shutdown, the thread
145    /// calling shutdown handles joining on these.
146    pub(super) worker_threads: HashMap<usize, thread::JoinHandle<()>>,
147    /// This is a counter used to iterate `worker_threads` in a consistent order (for loom's
148    /// benefit).
149    pub(super) worker_thread_index: usize,
150}
151
152impl ThreadManagementState {
153    /// Flag the pool as shutting down and hand back the worker `JoinHandle`s
154    /// to join. Returns `None` if shutdown has already begun. The caller is
155    /// responsible for waking all waiting workers.
156    pub(super) fn begin_shutdown(&mut self) -> Option<ShutdownHandles> {
157        if self.shutdown {
158            return None;
159        }
160        self.shutdown = true;
161        self.shutdown_tx = None;
162
163        let last_exited_thread = std::mem::take(&mut self.last_exiting_thread);
164        let workers = std::mem::take(&mut self.worker_threads);
165        Some((last_exited_thread, workers))
166    }
167
168    /// Bookkeeping for a worker exiting on its keep-alive timeout: leaves
169    /// its own handle for the next timed-out worker (or shutdown) to join,
170    /// and returns the previous timed-out thread's handle, which the caller
171    /// must join after releasing the lock.
172    pub(super) fn worker_timed_out(
173        &mut self,
174        worker_thread_id: usize,
175    ) -> Option<thread::JoinHandle<()>> {
176        let my_handle = self.worker_threads.remove(&worker_thread_id);
177        std::mem::replace(&mut self.last_exiting_thread, my_handle)
178    }
179}
180
181pub(crate) struct Task {
182    task: task::UnownedTask<BlockingSchedule>,
183    mandatory: Mandatory,
184}
185
186#[derive(PartialEq, Eq)]
187pub(crate) enum Mandatory {
188    #[cfg_attr(any(not(feature = "fs"), target_os = "emscripten"), allow(dead_code))]
189    Mandatory,
190    NonMandatory,
191}
192
193pub(crate) enum SpawnError {
194    /// Pool is shutting down and the task was not scheduled
195    ShuttingDown,
196    /// There are no worker threads available to take the task
197    /// and the OS failed to spawn a new one
198    NoThreads(io::Error),
199}
200
201impl From<SpawnError> for io::Error {
202    fn from(e: SpawnError) -> Self {
203        match e {
204            SpawnError::ShuttingDown => io::Error::other("blocking pool shutting down"),
205            SpawnError::NoThreads(e) => e,
206        }
207    }
208}
209
210impl Task {
211    pub(crate) fn new(task: task::UnownedTask<BlockingSchedule>, mandatory: Mandatory) -> Task {
212        Task { task, mandatory }
213    }
214
215    pub(super) fn shutdown(self) {
216        self.task.shutdown();
217    }
218
219    pub(super) fn run(self) {
220        self.task.run();
221    }
222
223    pub(super) fn shutdown_or_run_if_mandatory(self) {
224        match self.mandatory {
225            Mandatory::NonMandatory => self.task.shutdown(),
226            Mandatory::Mandatory => self.task.run(),
227        }
228    }
229}
230
231const KEEP_ALIVE: Duration = Duration::from_secs(10);
232
233/// Runs the provided function on an executor dedicated to blocking operations.
234/// Tasks will be scheduled as non-mandatory, meaning they may not get executed
235/// in case of runtime shutdown.
236#[track_caller]
237#[cfg_attr(target_os = "wasi", allow(dead_code))]
238pub(crate) fn spawn_blocking<F, R>(func: F) -> JoinHandle<R>
239where
240    F: FnOnce() -> R + Send + 'static,
241    R: Send + 'static,
242{
243    let rt = Handle::current();
244    rt.spawn_blocking(func)
245}
246
247cfg_io_blocking! {
248    #[cfg_attr(any(
249        all(loom, not(test)), // the function is covered by loom tests
250        test,
251        target_os = "emscripten", // fs and io-std use the inline shim
252    ), allow(dead_code))]
253    /// Runs the provided function on an executor dedicated to blocking
254    /// operations. Tasks will be scheduled as mandatory, meaning they are
255    /// guaranteed to run unless a shutdown is already taking place. In case a
256    /// shutdown is already taking place, `None` will be returned.
257    pub(crate) fn spawn_mandatory_blocking<F, R>(func: F) -> Option<JoinHandle<R>>
258    where
259        F: FnOnce() -> R + Send + 'static,
260        R: Send + 'static,
261    {
262        let rt = Handle::current();
263        rt.inner.blocking_spawner().spawn_mandatory_blocking(&rt, func)
264    }
265}
266
267// ===== impl BlockingPool =====
268
269impl BlockingPool {
270    pub(crate) fn new(
271        builder: &Builder,
272        thread_cap: usize,
273        scheduler_threads: usize,
274    ) -> BlockingPool {
275        let (shutdown_tx, shutdown_rx) = shutdown::channel();
276        let keep_alive = builder.keep_alive.unwrap_or(KEEP_ALIVE);
277
278        let thread_mgmt_state = ThreadManagementState {
279            shutdown: false,
280            shutdown_tx: Some(shutdown_tx),
281            last_exiting_thread: None,
282            worker_threads: HashMap::new(),
283            worker_thread_index: 0,
284        };
285
286        let inner_impl = if builder.sharded_blocking_queue {
287            InnerImpl::Sharded(ShardedImpl::new(thread_mgmt_state))
288        } else {
289            InnerImpl::Locked(LockedImpl::new(thread_mgmt_state))
290        };
291
292        BlockingPool {
293            spawner: Spawner {
294                inner: Arc::new(Inner {
295                    inner_impl,
296                    thread_name: builder.thread_name.clone(),
297                    stack_size: builder.thread_stack_size,
298                    after_start: builder.after_start.clone(),
299                    before_stop: builder.before_stop.clone(),
300                    thread_cap,
301                    scheduler_threads,
302                    keep_alive,
303                    metrics: SpawnerMetrics::default(),
304                }),
305            },
306            shutdown_rx,
307        }
308    }
309
310    pub(crate) fn spawner(&self) -> &Spawner {
311        &self.spawner
312    }
313
314    pub(crate) fn shutdown(&mut self, timeout: Option<Duration>) {
315        // The function can be called multiple times. First, by explicitly
316        // calling `shutdown` then by the drop handler calling `shutdown`. This
317        // prevents shutting down twice.
318        let inner = &self.spawner.inner;
319        let (last_exited_thread, workers) = match inner.inner_impl.begin_shutdown(&inner.metrics) {
320            Some(x) => x,
321            None => return,
322        };
323
324        if self.shutdown_rx.wait(timeout) {
325            let _ = last_exited_thread.map(thread::JoinHandle::join);
326
327            // Loom requires that execution be deterministic, so sort by thread ID before joining.
328            // (HashMaps use a randomly-seeded hash function, so the order is nondeterministic)
329            #[cfg(loom)]
330            let workers: Vec<(usize, thread::JoinHandle<()>)> = {
331                let mut workers: Vec<_> = workers.into_iter().collect();
332                workers.sort_by_key(|(id, _)| *id);
333                workers
334            };
335
336            for (_id, handle) in workers {
337                let _ = handle.join();
338            }
339        }
340    }
341}
342
343impl Drop for BlockingPool {
344    fn drop(&mut self) {
345        self.shutdown(None);
346    }
347}
348
349impl fmt::Debug for BlockingPool {
350    fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
351        fmt.debug_struct("BlockingPool").finish()
352    }
353}
354
355// ===== impl Spawner =====
356
357impl Spawner {
358    #[track_caller]
359    pub(crate) fn spawn_blocking<F, R>(&self, rt: &Handle, func: F) -> JoinHandle<R>
360    where
361        F: FnOnce() -> R + Send + 'static,
362        R: Send + 'static,
363    {
364        let fn_size = std::mem::size_of::<F>();
365        let (join_handle, spawn_result) = if AutoBox::<F>::SHOULD_BOX {
366            self.spawn_blocking_inner(
367                Box::new(func),
368                Mandatory::NonMandatory,
369                SpawnMeta::new_unnamed(fn_size),
370                rt,
371            )
372        } else {
373            self.spawn_blocking_inner(
374                func,
375                Mandatory::NonMandatory,
376                SpawnMeta::new_unnamed(fn_size),
377                rt,
378            )
379        };
380
381        match spawn_result {
382            Ok(()) => join_handle,
383            // Compat: do not panic here, return the join_handle even though it will never resolve
384            Err(SpawnError::ShuttingDown) => join_handle,
385            Err(SpawnError::NoThreads(e)) => {
386                panic!("OS can't spawn worker thread: {e}")
387            }
388        }
389    }
390
391    cfg_io_blocking! {
392        #[track_caller]
393        #[cfg_attr(any(
394            all(loom, not(test)), // the function is covered by loom tests
395            test,
396            target_os = "emscripten", // fs and io-std use the inline shim
397        ), allow(dead_code))]
398        pub(crate) fn spawn_mandatory_blocking<F, R>(&self, rt: &Handle, func: F) -> Option<JoinHandle<R>>
399        where
400            F: FnOnce() -> R + Send + 'static,
401            R: Send + 'static,
402        {
403            let fn_size = std::mem::size_of::<F>();
404            let (join_handle, spawn_result) = if AutoBox::<F>::SHOULD_BOX {
405                self.spawn_blocking_inner(
406                    Box::new(func),
407                    Mandatory::Mandatory,
408                    SpawnMeta::new_unnamed(fn_size),
409                    rt,
410                )
411            } else {
412                self.spawn_blocking_inner(
413                    func,
414                    Mandatory::Mandatory,
415                    SpawnMeta::new_unnamed(fn_size),
416                    rt,
417                )
418            };
419
420            if spawn_result.is_ok() {
421                Some(join_handle)
422            } else {
423                None
424            }
425        }
426    }
427
428    #[track_caller]
429    pub(crate) fn spawn_blocking_inner<F, R>(
430        &self,
431        func: F,
432        is_mandatory: Mandatory,
433        spawn_meta: SpawnMeta<'_>,
434        rt: &Handle,
435    ) -> (JoinHandle<R>, Result<(), SpawnError>)
436    where
437        F: FnOnce() -> R + Send + 'static,
438        R: Send + 'static,
439    {
440        let id = task::Id::next();
441        let fut =
442            blocking_task::<F, BlockingTask<F>>(BlockingTask::new(func), spawn_meta, id.as_u64());
443
444        let (task, handle) = task::unowned(
445            fut,
446            BlockingSchedule::new(rt),
447            id,
448            task::SpawnLocation::capture(),
449        );
450
451        let spawned = self.spawn_task(Task::new(task, is_mandatory), rt);
452        (handle, spawned)
453    }
454
455    pub(crate) fn num_blocking_threads(&self) -> usize {
456        self.inner
457            .metrics
458            .num_threads()
459            .saturating_sub(self.inner.scheduler_threads)
460    }
461
462    fn spawn_task(&self, task: Task, rt: &Handle) -> Result<(), SpawnError> {
463        // The `on_no_idle` closure runs under the same lock as the queue
464        // push, exactly like the pre-refactor code that called
465        // `self.spawn_thread` directly while holding `Mutex<ThreadManagementState>`.
466        self.inner
467            .inner_impl
468            .spawn_task(task, &self.inner.metrics, |thread_mgmt_state| {
469                // No threads are able to process the task.
470
471                if self.inner.metrics.num_threads() == self.inner.thread_cap {
472                    // At max number of threads
473                    return Ok(());
474                }
475
476                assert!(thread_mgmt_state.shutdown_tx.is_some());
477                let shutdown_tx = thread_mgmt_state.shutdown_tx.clone();
478
479                if let Some(shutdown_tx) = shutdown_tx {
480                    let id = thread_mgmt_state.worker_thread_index;
481
482                    match self.spawn_thread(shutdown_tx, rt, id) {
483                        Ok(handle) => {
484                            self.inner.metrics.inc_num_threads();
485                            thread_mgmt_state.worker_thread_index += 1;
486                            thread_mgmt_state.worker_threads.insert(id, handle);
487                        }
488                        Err(ref e)
489                            if is_temporary_os_thread_error(e)
490                                && self.num_blocking_threads() > 0 =>
491                        {
492                            // OS temporarily failed to spawn a new thread.
493                            // The task will be picked up eventually by a currently
494                            // busy thread.
495                        }
496                        Err(e) => {
497                            // The OS refused to spawn the thread and there is no thread
498                            // to pick up the task that has just been pushed to the queue.
499                            return Err(SpawnError::NoThreads(e));
500                        }
501                    }
502                }
503
504                Ok(())
505            })
506    }
507
508    fn spawn_thread(
509        &self,
510        shutdown_tx: shutdown::Sender,
511        rt: &Handle,
512        id: usize,
513    ) -> io::Result<thread::JoinHandle<()>> {
514        let mut builder = thread::Builder::new().name((self.inner.thread_name)());
515
516        if let Some(stack_size) = self.inner.stack_size {
517            builder = builder.stack_size(stack_size);
518        }
519
520        let rt = rt.clone();
521
522        builder.spawn(move || {
523            // Only the reference should be moved into the closure
524            let _enter = rt.enter();
525            rt.inner.blocking_spawner().inner.run(id);
526            drop(shutdown_tx);
527        })
528    }
529}
530
531cfg_unstable_metrics! {
532    impl Spawner {
533        pub(crate) fn num_idle_threads(&self) -> usize {
534            self.inner.metrics.num_idle_threads()
535        }
536
537        pub(crate) fn queue_depth(&self) -> usize {
538            self.inner.metrics.queue_depth()
539        }
540    }
541}
542
543// Each method on `InnerImpl` dispatches to the matching method on the
544// concrete variant. The variant methods own the entire critical section
545// for their operation, which makes it self-evident that the `Locked`
546// variant's behavior is identical to the pre-refactor code and gives a
547// future `Concurrent` variant a symmetric slot to fill.
548impl InnerImpl {
549    fn spawn_task<F>(
550        &self,
551        task: Task,
552        metrics: &SpawnerMetrics,
553        on_no_idle: F,
554    ) -> Result<(), SpawnError>
555    where
556        F: FnOnce(&mut ThreadManagementState) -> Result<(), SpawnError>,
557    {
558        match self {
559            InnerImpl::Locked(l) => l.spawn_task(task, metrics, on_no_idle),
560            InnerImpl::Sharded(s) => s.spawn_task(task, metrics, on_no_idle),
561        }
562    }
563
564    fn run_worker(
565        &self,
566        metrics: &SpawnerMetrics,
567        keep_alive: Duration,
568        worker_thread_id: usize,
569    ) -> Option<thread::JoinHandle<()>> {
570        match self {
571            InnerImpl::Locked(l) => l.run_worker(metrics, keep_alive, worker_thread_id),
572            InnerImpl::Sharded(s) => s.run_worker(metrics, keep_alive, worker_thread_id),
573        }
574    }
575
576    fn begin_shutdown(&self, metrics: &SpawnerMetrics) -> Option<ShutdownHandles> {
577        match self {
578            InnerImpl::Locked(l) => l.begin_shutdown(),
579            InnerImpl::Sharded(s) => s.begin_shutdown(metrics),
580        }
581    }
582}
583
584// This is the original, single-lock implementation of the `spawn_blocking`
585// queue. Method scope was principally designed around ensuring that when the
586// code was refactored to enable adding a sharded queue implementation, this
587// was self-evidently behaviorally identical to the original implementation.
588impl LockedImpl {
589    fn new(thread_mgmt_state: ThreadManagementState) -> LockedImpl {
590        LockedImpl {
591            mutex: Mutex::new(LockedInner {
592                queue: VecDeque::new(),
593                num_notify: 0,
594                thread_mgmt_state,
595            }),
596            condvar: Condvar::new(),
597        }
598    }
599
600    /// Push a task and either notify an idle worker or invoke
601    /// `on_no_idle` (which is responsible for spawning a new worker if
602    /// possible).
603    fn spawn_task<F>(
604        &self,
605        task: Task,
606        metrics: &SpawnerMetrics,
607        on_no_idle: F,
608    ) -> Result<(), SpawnError>
609    where
610        F: FnOnce(&mut ThreadManagementState) -> Result<(), SpawnError>,
611    {
612        let mut locked = self.mutex.lock();
613
614        if locked.thread_mgmt_state.shutdown {
615            drop(locked);
616
617            // Shutdown the task: it's fine to shutdown this task
618            // (even if mandatory) because it was scheduled after the
619            // shutdown of the runtime began.
620            task.task.shutdown();
621            return Err(SpawnError::ShuttingDown);
622        }
623
624        locked.queue.push_back(task);
625        metrics.inc_queue_depth();
626
627        if metrics.num_idle_threads() == 0 {
628            on_no_idle(&mut locked.thread_mgmt_state)?;
629        } else {
630            // Notify an idle worker thread. The notification counter
631            // is used to count the needed amount of notifications
632            // exactly. Thread libraries may generate spurious
633            // wakeups, this counter is used to keep us in a
634            // consistent state.
635            metrics.dec_num_idle_threads();
636            locked.num_notify += 1;
637            self.condvar.notify_one();
638        }
639
640        Ok(())
641    }
642
643    /// Run a worker thread's main loop.
644    fn run_worker(
645        &self,
646        metrics: &SpawnerMetrics,
647        keep_alive: Duration,
648        worker_thread_id: usize,
649    ) -> Option<thread::JoinHandle<()>> {
650        let mut locked = self.mutex.lock();
651        let mut join_on_thread = None;
652        // is this thread currently counted in `num_idle_threads`?
653        let mut is_counted_idle;
654
655        'main: loop {
656            // BUSY
657            while let Some(task) = locked.queue.pop_front() {
658                metrics.dec_queue_depth();
659                drop(locked);
660                task.run();
661
662                locked = self.mutex.lock();
663            }
664
665            // IDLE
666            metrics.inc_num_idle_threads();
667            // mark this thread as currently counted in `num_idle_threads`.
668            is_counted_idle = true;
669
670            while !locked.thread_mgmt_state.shutdown {
671                let lock_result = self.condvar.wait_timeout(locked, keep_alive).unwrap();
672
673                locked = lock_result.0;
674                let timeout_result = lock_result.1;
675
676                if locked.num_notify != 0 {
677                    // We have received a legitimate wakeup,
678                    // acknowledge it by decrementing the counter
679                    // and transition to the BUSY state.
680                    locked.num_notify -= 1;
681                    // since this is a legitimate wakeup,
682                    // the `Spawner::spawn_task` has already
683                    // decremented `num_idle_threads`.
684                    is_counted_idle = false;
685                    break;
686                }
687
688                // Even if the condvar "timed out", if the pool is
689                // entering the shutdown phase, we want to perform
690                // the cleanup logic.
691                if !locked.thread_mgmt_state.shutdown && timeout_result.timed_out() {
692                    join_on_thread = locked.thread_mgmt_state.worker_timed_out(worker_thread_id);
693
694                    break 'main;
695                }
696
697                // Spurious wakeup detected, go back to sleep.
698            }
699
700            if locked.thread_mgmt_state.shutdown {
701                // Drain the queue
702                while let Some(task) = locked.queue.pop_front() {
703                    metrics.dec_queue_depth();
704                    drop(locked);
705
706                    task.shutdown_or_run_if_mandatory();
707
708                    locked = self.mutex.lock();
709                }
710
711                break;
712            }
713        }
714
715        // Thread exit
716        metrics.dec_num_threads();
717
718        // Is this thread currently counted in `num_idle_threads`?
719        if is_counted_idle {
720            // `num_idle_threads` should now be tracked exactly,
721            // panic with a descriptive message if it is not the
722            // case.
723            let prev_idle = metrics.dec_num_idle_threads();
724            assert_ne!(
725                prev_idle, 0,
726                "`num_idle_threads` underflowed on thread exit"
727            );
728        }
729
730        if locked.thread_mgmt_state.shutdown && metrics.num_threads() == 0 {
731            self.condvar.notify_one();
732        }
733
734        drop(locked);
735
736        join_on_thread
737    }
738
739    /// Begin pool shutdown: set the shutdown flag, drop the shutdown
740    /// sender, wake all waiting workers, and hand back the worker
741    /// `JoinHandle`s for the caller to join.
742    fn begin_shutdown(&self) -> Option<ShutdownHandles> {
743        let mut locked = self.mutex.lock();
744        let handles = locked.thread_mgmt_state.begin_shutdown()?;
745        self.condvar.notify_all();
746        Some(handles)
747    }
748}
749
750// Tells whether the error when spawning a thread is temporary.
751#[inline]
752fn is_temporary_os_thread_error(error: &io::Error) -> bool {
753    matches!(error.kind(), io::ErrorKind::WouldBlock)
754}
755
756impl Inner {
757    fn run(&self, worker_thread_id: usize) {
758        if let Some(f) = &self.after_start {
759            f();
760        }
761
762        let join_on_thread =
763            self.inner_impl
764                .run_worker(&self.metrics, self.keep_alive, worker_thread_id);
765
766        if let Some(f) = &self.before_stop {
767            f();
768        }
769
770        if let Some(handle) = join_on_thread {
771            let _ = handle.join();
772        }
773    }
774}
775
776impl fmt::Debug for Spawner {
777    fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
778        fmt.debug_struct("blocking::Spawner").finish()
779    }
780}