use std::collections::VecDeque;
use std::marker::PhantomData;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::{Arc, Condvar, Mutex};
use std::thread::JoinHandle;
type Task = Box<dyn FnOnce() + Send>;
struct Shared {
queue: Mutex<VecDeque<Task>>,
ready: Condvar,
shutdown: AtomicBool,
}
impl Shared {
fn push(&self, task: Task) {
self.queue.lock().unwrap().push_back(task);
self.ready.notify_one();
}
fn try_pop(&self) -> Option<Task> {
self.queue.lock().unwrap().pop_front()
}
}
pub(crate) struct ThreadPool {
shared: Arc<Shared>,
workers: usize,
handles: Vec<JoinHandle<()>>,
}
impl ThreadPool {
pub(crate) fn new(workers: usize) -> Self {
let shared = Arc::new(Shared {
queue: Mutex::new(VecDeque::new()),
ready: Condvar::new(),
shutdown: AtomicBool::new(false),
});
let mut handles = Vec::with_capacity(workers);
for _ in 0..workers {
let shared = shared.clone();
if let Ok(h) = std::thread::Builder::new()
.name("hpvca-pool".into())
.spawn(move || worker_loop(&shared))
{
handles.push(h);
}
}
let workers = handles.len();
ThreadPool {
shared,
workers,
handles,
}
}
pub(crate) fn parallelism(&self) -> usize {
self.workers + 1
}
pub(crate) fn scoped<'scope, F, R>(&'scope self, f: F) -> R
where
F: FnOnce(&Scope<'scope>) -> R,
{
let scope = Scope {
shared: &self.shared,
pending: AtomicUsize::new(0),
panicked: AtomicBool::new(false),
_marker: PhantomData,
};
let out = f(&scope);
scope.wait();
out
}
}
impl Drop for ThreadPool {
fn drop(&mut self) {
{
let _guard = self.shared.queue.lock().unwrap();
self.shared.shutdown.store(true, Ordering::Release);
}
self.shared.ready.notify_all();
for h in self.handles.drain(..) {
let _ = h.join();
}
}
}
fn worker_loop(shared: &Shared) {
loop {
let task = {
let mut q = shared.queue.lock().unwrap();
loop {
if let Some(t) = q.pop_front() {
break Some(t);
}
if shared.shutdown.load(Ordering::Acquire) {
break None;
}
q = shared.ready.wait(q).unwrap();
}
};
match task {
Some(t) => t(),
None => return,
}
}
}
pub(crate) struct Scope<'scope> {
shared: &'scope Shared,
pending: AtomicUsize,
panicked: AtomicBool,
_marker: PhantomData<&'scope ()>,
}
struct PendingGuard<'a> {
pending: &'a AtomicUsize,
panicked: &'a AtomicBool,
}
impl Drop for PendingGuard<'_> {
fn drop(&mut self) {
if std::thread::panicking() {
self.panicked.store(true, Ordering::SeqCst);
}
self.pending.fetch_sub(1, Ordering::SeqCst);
}
}
impl<'scope> Scope<'scope> {
pub(crate) fn spawn<F>(&self, task: F)
where
F: FnOnce() + Send + 'scope,
{
self.pending.fetch_add(1, Ordering::SeqCst);
let pending: &'scope AtomicUsize = unsafe { &*(&self.pending as *const AtomicUsize) };
let panicked: &'scope AtomicBool = unsafe { &*(&self.panicked as *const AtomicBool) };
let job: Box<dyn FnOnce() + Send + 'scope> = Box::new(move || {
let _guard = PendingGuard { pending, panicked };
task();
});
let job: Task =
unsafe { std::mem::transmute::<Box<dyn FnOnce() + Send + 'scope>, Task>(job) };
self.shared.push(job);
}
fn wait(&self) {
const STALL_LIMIT: u64 = 20_000_000;
let mut idle_spins: u64 = 0;
while self.pending.load(Ordering::SeqCst) != 0 {
match self.shared.try_pop() {
Some(task) => {
idle_spins = 0;
task();
}
None => {
idle_spins += 1;
assert!(
idle_spins < STALL_LIMIT,
"hpvca thread pool stalled: {} task(s) still pending with an empty \
queue after {} idle spins — a task neither finished nor unwound",
self.pending.load(Ordering::SeqCst),
idle_spins,
);
if idle_spins < 1024 {
std::hint::spin_loop();
std::thread::yield_now();
} else {
std::thread::sleep(std::time::Duration::from_micros(50));
}
}
}
}
assert!(
!self.panicked.load(Ordering::SeqCst),
"hpvca thread pool: a worker task panicked (see the panic printed by the \
worker thread above)"
);
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::atomic::AtomicUsize;
#[test]
fn panicking_task_does_not_hang_the_scope() {
let pool = ThreadPool::new(2);
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
pool.scoped(|scope| {
for i in 0..2 {
scope.spawn(move || {
std::thread::sleep(std::time::Duration::from_millis(30));
panic!("worker task {i} fails on purpose");
});
}
std::thread::sleep(std::time::Duration::from_millis(150));
});
}));
assert!(
result.is_err(),
"a task that panicked on a worker must surface as a panic, not a hang"
);
}
#[test]
fn rapid_pool_create_drop_does_not_lose_shutdown_wakeup() {
for _ in 0..3000 {
let pool = ThreadPool::new(4);
drop(pool);
}
}
#[test]
fn all_tasks_run_to_completion() {
let pool = ThreadPool::new(3);
let counter = AtomicUsize::new(0);
pool.scoped(|scope| {
for _ in 0..64 {
scope.spawn(|| {
counter.fetch_add(1, Ordering::SeqCst);
});
}
});
assert_eq!(counter.load(Ordering::SeqCst), 64);
}
#[test]
fn zero_worker_pool_still_drains() {
let pool = ThreadPool::new(0);
let counter = AtomicUsize::new(0);
pool.scoped(|scope| {
for _ in 0..16 {
scope.spawn(|| {
counter.fetch_add(1, Ordering::SeqCst);
});
}
});
assert_eq!(counter.load(Ordering::SeqCst), 16);
}
}