#![allow(clippy::tabs_in_doc_comments)]
use std::{
collections::VecDeque,
fmt, mem,
sync::{Arc, Condvar, Mutex},
thread::{self, JoinHandle},
};
use tracing::debug;
#[derive(Debug)]
pub struct ThreadPool<WorkerData: Send + 'static = ()> {
inner: Arc<ThreadPoolShared<WorkerData>>,
workers: Vec<JoinHandle<()>>,
}
type Task<WorkerData> = Box<dyn FnOnce(&mut WorkerData) + Send>;
enum PoolQueue<WorkerData: Send + 'static> {
Done,
Todo(VecDeque<Task<WorkerData>>),
}
impl<WorkerData: Send + 'static> fmt::Debug for PoolQueue<WorkerData> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Done => write!(f, "Done"),
Self::Todo(ref tasks) => write!(f, "Todo({})", tasks.len()),
}
}
}
enum DequeueResult<WorkerData> {
Joined,
WaitingForTasks,
TaskAvailable {
task: Task<WorkerData>,
has_more: bool,
},
}
impl<WorkerData: Send + 'static> PoolQueue<WorkerData> {
fn dequeue(&mut self) -> DequeueResult<WorkerData> {
match self {
Self::Done => DequeueResult::Joined,
Self::Todo(ref mut tasks) => match tasks.pop_front() {
Some(task) => DequeueResult::TaskAvailable {
task,
has_more: !tasks.is_empty(),
},
None => DequeueResult::WaitingForTasks,
},
}
}
}
#[derive(Debug)]
struct ThreadPoolShared<WorkerData: Send + 'static> {
workers_condvar: Condvar,
pool_condvar: Condvar,
max_pending_tasks: usize,
pending_tasks: Mutex<PoolQueue<WorkerData>>,
}
impl<WorkerData: Send + 'static> ThreadPool<WorkerData> {
#[must_use]
pub fn new(workers_data: Vec<WorkerData>) -> Self {
let max_pending = workers_data.len();
Self::new_with_queue_size(workers_data, max_pending)
}
#[must_use]
pub fn new_with_queue_size(workers_data: Vec<WorkerData>, max_pending_tasks: usize) -> Self {
assert_ne!(
workers_data.len(),
0,
"workers_data must contain at least one item"
);
assert_ne!(
max_pending_tasks, 0,
"max_pending_tasks must be greater than 0"
);
let inner = Arc::new(ThreadPoolShared {
workers_condvar: Condvar::default(),
pool_condvar: Condvar::default(),
pending_tasks: Mutex::new(PoolQueue::Todo(VecDeque::new())),
max_pending_tasks,
});
let workers = workers_data
.into_iter()
.enumerate()
.map(|(i, mut worker_data)| {
let inner_clone = inner.clone();
thread::Builder::new()
.name(format!("w({i})"))
.spawn(move || loop {
let ThreadPoolShared {
pending_tasks,
workers_condvar,
pool_condvar,
..
} = &*inner_clone;
let mut guard = pending_tasks.lock().unwrap();
let dequeued = loop {
match guard.dequeue() {
DequeueResult::Joined => break None,
DequeueResult::WaitingForTasks => {
debug!("waiting for tasks...");
guard = workers_condvar.wait(guard).unwrap();
}
dequeued @ DequeueResult::TaskAvailable { .. } => {
break Some(dequeued)
}
}
};
if let Some(DequeueResult::TaskAvailable { task, has_more }) = dequeued {
pool_condvar.notify_all();
drop(guard);
if has_more {
workers_condvar.notify_all();
}
debug!("running task...");
(task)(&mut worker_data);
} else {
debug!("quitting...");
break;
}
})
.expect("thread to be spawned")
})
.collect::<Vec<_>>();
Self { inner, workers }
}
#[allow(clippy::missing_panics_doc)]
pub fn enqueue<Task: FnOnce(&mut WorkerData) + Send + 'static>(&mut self, task: Task) {
let mut guard = self.inner.pending_tasks.lock().unwrap();
loop {
match &mut *guard {
PoolQueue::Todo(ref mut tasks) => {
if tasks.len() >= self.inner.max_pending_tasks {
debug!("waiting for available workers...");
guard = self.inner.pool_condvar.wait(guard).unwrap();
} else {
tasks.push_back(Box::new(task));
self.inner.workers_condvar.notify_one();
debug!("added pending task");
return;
}
}
PoolQueue::Done => unreachable!(
"enqueue shouldn't be callable on a joined (thus consumed) thread pool"
),
}
}
}
pub fn join(mut self) {
self.join_by_ref();
}
fn join_by_ref(&mut self) {
let mut guard = self.inner.pending_tasks.lock().unwrap();
loop {
match &mut *guard {
PoolQueue::Done => return,
PoolQueue::Todo(tasks) if tasks.is_empty() => break,
PoolQueue::Todo(_) => {
debug!("waiting for idle...");
guard = self.inner.pool_condvar.wait(guard).unwrap();
}
}
}
debug!("sending stop request...");
*guard = PoolQueue::Done;
drop(guard);
self.inner.workers_condvar.notify_all();
debug!("joining...");
let workers = mem::take(&mut self.workers);
for w in workers {
w.join().unwrap();
}
}
}
impl<WorkerData: Send + 'static> Drop for ThreadPool<WorkerData> {
fn drop(&mut self) {
self.join_by_ref();
}
}