use std::any::Any;
use std::collections::VecDeque;
use std::sync::{Arc, Condvar, Mutex};
use std::thread::JoinHandle;
use bevy_ecs::prelude::Resource;
type Job = Box<dyn FnOnce() + Send + 'static>;
struct Queue {
jobs: VecDeque<Job>,
closed: bool,
}
struct Inner {
queue: Mutex<Queue>,
signal: Condvar,
}
enum SlotState<T> {
Empty,
Done(T),
Failed(Box<dyn Any + Send + 'static>),
}
struct Slot<T> {
value: Mutex<SlotState<T>>,
ready: Condvar,
}
pub struct Task<T> {
slot: Arc<Slot<T>>,
}
impl<T> Task<T> {
pub fn join(self) -> T {
let mut guard = self.slot.value.lock().unwrap_or_else(|e| e.into_inner());
loop {
if !matches!(&*guard, SlotState::Empty) {
break;
}
guard = self
.slot
.ready
.wait(guard)
.unwrap_or_else(|e| e.into_inner());
}
match std::mem::replace(&mut *guard, SlotState::Empty) {
SlotState::Done(value) => value,
SlotState::Failed(payload) => std::panic::resume_unwind(payload),
SlotState::Empty => unreachable!("wait returned on an empty slot"),
}
}
pub fn is_finished(&self) -> bool {
!matches!(
&*self.slot.value.lock().unwrap_or_else(|e| e.into_inner()),
SlotState::Empty
)
}
}
#[derive(Resource)]
pub struct TaskPool {
inner: Arc<Inner>,
workers: Vec<JoinHandle<()>>,
}
impl TaskPool {
pub fn new(threads: usize) -> Self {
let threads = threads.max(1);
let inner = Arc::new(Inner {
queue: Mutex::new(Queue {
jobs: VecDeque::new(),
closed: false,
}),
signal: Condvar::new(),
});
let mut workers = Vec::with_capacity(threads);
for index in 0..threads {
let inner = Arc::clone(&inner);
let handle = std::thread::Builder::new()
.name(format!("repame-task-{index}"))
.spawn(move || worker(inner))
.expect("failed to start a task-pool worker");
workers.push(handle);
}
Self { inner, workers }
}
pub fn auto() -> Self {
let threads = std::thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(1);
Self::new(threads)
}
pub fn threads(&self) -> usize {
self.workers.len()
}
pub fn spawn<F, T>(&self, job: F) -> Task<T>
where
F: FnOnce() -> T + Send + 'static,
T: Send + 'static,
{
let slot = Arc::new(Slot {
value: Mutex::new(SlotState::Empty),
ready: Condvar::new(),
});
let done = Arc::clone(&slot);
self.push(Box::new(move || {
let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(job));
let mut guard = done.value.lock().unwrap_or_else(|e| e.into_inner());
*guard = match outcome {
Ok(value) => SlotState::Done(value),
Err(payload) => SlotState::Failed(payload),
};
done.ready.notify_all();
}));
Task { slot }
}
pub fn map<I, F, T>(&self, items: I, job: F) -> Vec<T>
where
I: IntoIterator,
F: Fn(I::Item) -> T + Send + Sync + 'static,
I::Item: Send + 'static,
T: Send + 'static,
{
let job = Arc::new(job);
let mut tasks = Vec::new();
for item in items {
let job = Arc::clone(&job);
tasks.push(self.spawn(move || job(item)));
}
tasks.into_iter().map(Task::join).collect()
}
fn push(&self, job: Job) {
let mut queue = self.inner.queue.lock().unwrap_or_else(|e| e.into_inner());
if queue.closed {
return;
}
queue.jobs.push_back(job);
self.inner.signal.notify_one();
}
}
impl Drop for TaskPool {
fn drop(&mut self) {
{
let mut queue = self.inner.queue.lock().unwrap_or_else(|e| e.into_inner());
queue.closed = true;
}
self.inner.signal.notify_all();
for worker in self.workers.drain(..) {
let _ = worker.join();
}
}
}
fn worker(inner: Arc<Inner>) {
loop {
let job = {
let mut queue = inner.queue.lock().unwrap_or_else(|e| e.into_inner());
loop {
if let Some(job) = queue.jobs.pop_front() {
break Some(job);
}
if queue.closed {
break None;
}
queue = inner.signal.wait(queue).unwrap_or_else(|e| e.into_inner());
}
};
match job {
Some(job) => job(),
None => return,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::atomic::{AtomicUsize, Ordering};
#[test]
fn spawn_joins_to_the_value() {
let pool = TaskPool::new(2);
assert_eq!(pool.spawn(|| 6 * 7).join(), 42);
assert_eq!(pool.threads(), 2);
}
#[test]
fn one_worker_is_the_floor() {
let pool = TaskPool::new(0);
assert_eq!(pool.threads(), 1);
assert_eq!(pool.spawn(|| 1 + 1).join(), 2);
}
#[test]
fn map_preserves_input_order() {
let pool = TaskPool::new(4);
let out = pool.map(0..64, |n| n * n);
assert_eq!(out, (0..64).map(|n| n * n).collect::<Vec<_>>());
}
#[test]
fn jobs_actually_run_off_thread() {
let pool = TaskPool::new(3);
let counter = Arc::new(AtomicUsize::new(0));
let mut tasks = Vec::new();
for _ in 0..32 {
let counter = Arc::clone(&counter);
tasks.push(pool.spawn(move || {
counter.fetch_add(1, Ordering::SeqCst);
}));
}
for task in tasks {
task.join();
}
assert_eq!(counter.load(Ordering::SeqCst), 32);
}
#[test]
#[should_panic(expected = "boom")]
fn a_panicking_job_re_raises_at_the_joiner() {
let pool = TaskPool::new(1);
pool.spawn(|| panic!("boom")).join();
}
#[test]
fn a_panicked_worker_does_not_stop_the_pool() {
let pool = TaskPool::new(2);
let failed = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
pool.spawn(|| panic!("boom")).join();
}));
assert!(failed.is_err());
assert_eq!(pool.spawn(|| 3).join(), 3);
}
#[test]
fn dropping_the_pool_joins_its_workers() {
let pool = TaskPool::new(2);
let counter = Arc::new(AtomicUsize::new(0));
let c = Arc::clone(&counter);
let task = pool.spawn(move || {
c.fetch_add(1, Ordering::SeqCst);
});
task.join();
drop(pool);
assert_eq!(counter.load(Ordering::SeqCst), 1);
}
}