use std::num::NonZero;
use std::panic::{RefUnwindSafe, UnwindSafe};
use std::sync::{Arc, Mutex, mpsc};
use std::thread::{self, JoinHandle};
use std::{iter, mem};
use many_cpus::ProcessorSet;
#[derive(Debug)]
pub struct ThreadPool {
command_txs: Vec<mpsc::Sender<Command>>,
join_handles: Vec<JoinHandle<()>>,
thread_count: NonZero<usize>,
}
impl UnwindSafe for ThreadPool {}
impl RefUnwindSafe for ThreadPool {}
impl ThreadPool {
#[must_use]
pub fn new(processors: impl AsRef<ProcessorSet>) -> Self {
let processors = processors.as_ref();
let (txs, rxs): (Vec<_>, Vec<_>) = iter::repeat_with(mpsc::channel)
.take(processors.len())
.unzip();
let rxs = Arc::new(Mutex::new(rxs));
let join_handles = processors
.spawn_threads({
let rxs = Arc::clone(&rxs);
move |_| {
let rx = rxs
.lock()
.expect("no worker thread panics while holding this lock")
.pop()
.expect("one receiver per spawned thread");
worker_entrypoint(&rx);
}
})
.into_vec();
Self {
thread_count: NonZero::new(txs.len())
.expect("guarded by fact that ProcessorSet is never empty"),
command_txs: txs,
join_handles,
}
}
#[must_use]
#[cfg_attr(test, mutants::skip)] pub fn thread_count(&self) -> NonZero<usize> {
self.thread_count
}
#[cfg_attr(test, mutants::skip)] #[expect(
clippy::needless_pass_by_ref_mut,
reason = "protects users from deadlock through concurrent usage"
)]
pub(crate) fn execute_task<'f, F, R>(&mut self, f: F) -> Box<[R]>
where
F: FnOnce() -> R + Clone + Send + 'f,
R: Send + 'static,
{
let mut results = Vec::with_capacity(self.thread_count.get());
let (mut result_txs, result_rxs): (Vec<_>, Vec<_>) =
iter::repeat_with(oneshot::channel::<R>)
.take(self.thread_count.get())
.unzip();
for tx in &self.command_txs {
let f: Box<dyn FnOnce() -> R + Send + 'f> = Box::new(f.clone());
let f = unsafe {
mem::transmute::<
Box<dyn FnOnce() -> R + Send + 'f>,
Box<dyn FnOnce() -> R + Send + 'static>,
>(f)
};
tx.send(Command::Execute(Box::new({
let result_tx = result_txs
.pop()
.expect("type invariant - one command_tx per thread");
move || {
let result = f();
result_tx.send(result).expect(
"receiver must still exist - this is mandatory for scoped lifetime logic",
);
}
})))
.expect("worker thread must still exist - thread pool cannot operate without workers");
}
for rx in result_rxs {
results.push(
rx.recv()
.expect("worker thread failed to send result - did it panic?"),
);
}
results.into_boxed_slice()
}
}
impl Drop for ThreadPool {
#[cfg_attr(test, mutants::skip)] fn drop(&mut self) {
if thread::panicking() {
return;
}
for tx in self.command_txs.drain(..) {
tx.send(Command::Shutdown)
.expect("worker channel is open during orderly shutdown");
}
for handle in self.join_handles.drain(..) {
handle
.join()
.expect("worker thread completes orderly shutdown without panicking");
}
}
}
enum Command {
Execute(Box<dyn FnOnce() + Send>),
Shutdown,
}
#[cfg_attr(test, mutants::skip)] fn worker_entrypoint(rx: &mpsc::Receiver<Command>) {
while let Command::Execute(f) = rx
.recv()
.expect("command channel is open while pool exists")
{
f();
}
}
#[cfg(test)]
#[cfg_attr(coverage_nightly, coverage(off))]
mod tests {
use std::panic::{RefUnwindSafe, UnwindSafe};
use std::sync::atomic::{self, AtomicUsize};
use many_cpus::SystemHardware;
use new_zealand::nz;
use super::*;
static_assertions::assert_impl_all!(ThreadPool: UnwindSafe, RefUnwindSafe);
#[test]
fn smoke_test_all() {
let expected_default = SystemHardware::current().processors();
let expected_thread_count = expected_default.len();
let mut pool = ThreadPool::new(SystemHardware::current().processors());
assert_eq!(pool.thread_count().get(), expected_thread_count);
let counter = Arc::new(AtomicUsize::new(0));
pool.execute_task({
let counter = Arc::clone(&counter);
move || {
counter.fetch_add(1, atomic::Ordering::SeqCst);
}
});
assert_eq!(
counter.load(atomic::Ordering::SeqCst),
expected_thread_count
);
}
#[test]
fn smoke_test_one() {
let processor_set = SystemHardware::current()
.processors()
.to_builder()
.take(nz!(1))
.unwrap();
let expected_thread_count = processor_set.len();
let mut pool = ThreadPool::new(&processor_set);
assert_eq!(pool.thread_count().get(), expected_thread_count);
let counter = Arc::new(AtomicUsize::new(0));
pool.execute_task({
let counter = Arc::clone(&counter);
move || {
counter.fetch_add(1, atomic::Ordering::SeqCst);
}
});
assert_eq!(
counter.load(atomic::Ordering::SeqCst),
expected_thread_count
);
}
}