use std::cell::UnsafeCell;
use std::panic::{AssertUnwindSafe, catch_unwind, resume_unwind};
use std::sync::OnceLock;
use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering};
use std::thread::Thread;
type Job = dyn Fn(usize) + Sync + 'static;
const SPIN: u32 = 1 << 12;
const WAYS_BITS: u32 = 16;
struct Pool {
word: AtomicU64,
job: UnsafeCell<*const Job>,
pending: AtomicUsize,
panicked: AtomicBool,
busy: AtomicBool,
workers: UnsafeCell<Vec<Thread>>,
}
unsafe impl Sync for Pool {}
unsafe impl Send for Pool {}
static POOL: OnceLock<Pool> = OnceLock::new();
fn pool() -> &'static Pool {
POOL.get_or_init(|| Pool {
word: AtomicU64::new(0),
job: UnsafeCell::new(std::ptr::null::<fn(usize)>() as *const Job),
pending: AtomicUsize::new(0),
panicked: AtomicBool::new(false),
busy: AtomicBool::new(false),
workers: UnsafeCell::new(Vec::new()),
})
}
fn worker(pool: &'static Pool, share: usize, mut seen: u64) {
loop {
let mut spins = 0u32;
let word = loop {
let w = pool.word.load(Ordering::Acquire);
if w >> WAYS_BITS != seen {
break w;
}
if spins < SPIN {
spins += 1;
std::hint::spin_loop();
} else {
std::thread::park();
}
};
seen = word >> WAYS_BITS;
if share < (word & ((1 << WAYS_BITS) - 1)) as usize {
let job = unsafe { &**pool.job.get() };
if catch_unwind(AssertUnwindSafe(|| job(share))).is_err() {
pool.panicked.store(true, Ordering::Relaxed);
}
pool.pending.fetch_sub(1, Ordering::Release);
}
}
}
pub(super) fn run(ways: usize, job: &(dyn Fn(usize) + Sync)) {
let want = ways;
let ways = ways.min((1 << WAYS_BITS) - 1);
if want <= 1 {
job(0);
return;
}
#[cfg(test)]
DISPATCHES.fetch_add(1, Ordering::Relaxed);
let pool = pool();
let serial = || (0..want).for_each(job);
if pool.busy.swap(true, Ordering::Acquire) {
serial();
return;
}
let workers = unsafe { &mut *pool.workers.get() };
let epoch = pool.word.load(Ordering::Relaxed) >> WAYS_BITS;
while workers.len() < ways - 1 {
let share = workers.len() + 1;
let spawned = std::thread::Builder::new()
.name(format!("earthsci-tape-{share}"))
.spawn(move || worker(pool, share, epoch));
match spawned {
Ok(h) => workers.push(h.thread().clone()),
Err(_) => break,
}
}
let ways = ways.min(workers.len() + 1);
if ways <= 1 {
pool.busy.store(false, Ordering::Release);
serial();
return;
}
unsafe {
*pool.job.get() = std::mem::transmute::<&(dyn Fn(usize) + Sync), *const Job>(job);
}
pool.pending.store(ways - 1, Ordering::Relaxed);
pool.word
.store((epoch + 1) << WAYS_BITS | ways as u64, Ordering::Release);
for t in &workers[..ways - 1] {
t.unpark();
}
let own = catch_unwind(AssertUnwindSafe(|| {
job(0);
(ways..want).for_each(job);
}));
let mut spins = 0u32;
while pool.pending.load(Ordering::Acquire) != 0 {
if spins < SPIN {
spins += 1;
std::hint::spin_loop();
} else {
std::thread::yield_now();
}
}
let panicked = pool.panicked.swap(false, Ordering::Relaxed);
pool.busy.store(false, Ordering::Release);
if let Err(p) = own {
resume_unwind(p);
}
assert!(
!panicked,
"a worker of a split right-hand-side call panicked"
);
}
#[cfg(test)]
pub(crate) static DISPATCHES: AtomicUsize = AtomicUsize::new(0);
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn every_share_runs_exactly_once() {
for ways in [1, 2, 3, 5, 9] {
let hits: Vec<AtomicUsize> = (0..ways).map(|_| AtomicUsize::new(0)).collect();
run(ways, &|s| {
hits[s].fetch_add(1, Ordering::Relaxed);
});
assert!(
hits.iter().all(|h| h.load(Ordering::Relaxed) == 1),
"ways = {ways}"
);
}
}
}