#![allow(clippy::missing_const_for_thread_local)]
use crate::pool::{Job, ThreadPool};
use std::cell::RefCell;
use std::fmt;
use std::io;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, OnceLock};
#[cfg(feature = "rayon")]
use std::sync::{Mutex, mpsc};
use std::time::Duration;
thread_local! {
static ACTIVE_RUNTIMES: RefCell<Vec<u64>> = const { RefCell::new(Vec::new()) };
}
pub type ParallelJob = Box<dyn FnOnce() + Send + 'static>;
pub trait ParallelExecutor: Send + Sync + 'static {
fn parallelism(&self) -> usize;
fn try_execute(&self, job: ParallelJob, busy_timeout: Option<Duration>) -> io::Result<()>;
}
#[cfg(feature = "rayon")]
#[derive(Clone)]
pub struct RayonExecutor {
pool: Arc<rayon::ThreadPool>,
}
#[cfg(feature = "rayon")]
impl RayonExecutor {
#[must_use]
pub fn new(pool: Arc<rayon::ThreadPool>) -> Self {
Self { pool }
}
#[must_use]
pub fn pool(&self) -> &Arc<rayon::ThreadPool> {
&self.pool
}
}
#[cfg(feature = "rayon")]
impl ParallelExecutor for RayonExecutor {
fn parallelism(&self) -> usize {
self.pool.current_num_threads().max(1)
}
fn try_execute(&self, job: ParallelJob, busy_timeout: Option<Duration>) -> io::Result<()> {
let Some(timeout) = busy_timeout else {
self.pool.spawn(job);
return Ok(());
};
let pending = Arc::new(Mutex::new(Some(job)));
let worker_pending = Arc::clone(&pending);
let (started_sender, started_receiver) = mpsc::sync_channel(0);
self.pool.spawn(move || {
let job = worker_pending
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.take();
if let Some(job) = job {
let _ = started_sender.send(());
job();
}
});
match started_receiver.recv_timeout(timeout) {
Ok(()) => Ok(()),
Err(mpsc::RecvTimeoutError::Timeout) => {
let cancelled = pending
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.take()
.is_some();
if cancelled {
Err(io::Error::new(
io::ErrorKind::TimedOut,
"Rayon pool did not start the job before busy timeout",
))
} else {
Ok(())
}
}
Err(mpsc::RecvTimeoutError::Disconnected) => Err(io::Error::new(
io::ErrorKind::BrokenPipe,
"Rayon pool dropped the scheduled job",
)),
}
}
}
enum Executor {
Global,
Dedicated(Arc<ThreadPool>),
External(Arc<dyn ParallelExecutor>),
}
struct RuntimeInner {
id: u64,
executor: Executor,
busy_timeout: Option<Duration>,
}
#[derive(Clone)]
pub struct ParallelRuntime {
inner: Arc<RuntimeInner>,
}
impl ParallelRuntime {
#[must_use]
pub fn global() -> Self {
static RUNTIME: OnceLock<ParallelRuntime> = OnceLock::new();
RUNTIME
.get_or_init(|| Self::new(Executor::Global, None))
.clone()
}
pub fn dedicated(parallelism: usize) -> io::Result<Self> {
let pool = ThreadPool::with_workers(parallelism.max(1))?;
Ok(Self::new(Executor::Dedicated(Arc::new(pool)), None))
}
#[must_use]
pub fn external(executor: Arc<dyn ParallelExecutor>) -> Self {
Self::new(Executor::External(executor), None)
}
#[cfg(feature = "rayon")]
#[must_use]
pub fn rayon_existing(pool: Arc<rayon::ThreadPool>) -> Self {
Self::external(Arc::new(RayonExecutor::new(pool)))
.with_busy_timeout(Some(Duration::from_secs(1)))
}
#[cfg(feature = "rayon")]
pub fn rayon_new(parallelism: usize) -> io::Result<Self> {
let pool = rayon::ThreadPoolBuilder::new()
.num_threads(parallelism.max(1))
.build()
.map_err(io::Error::other)?;
Ok(Self::rayon_existing(Arc::new(pool)))
}
#[must_use]
pub fn with_busy_timeout(mut self, busy_timeout: Option<Duration>) -> Self {
Arc::make_mut(&mut self.inner).busy_timeout = busy_timeout;
self
}
#[must_use]
pub fn parallelism(&self) -> usize {
match &self.inner.executor {
Executor::Global => ThreadPool::global().workers(),
Executor::Dedicated(pool) => pool.workers(),
Executor::External(executor) => executor.parallelism().max(1),
}
}
pub(crate) fn is_worker_thread(&self) -> bool {
ACTIVE_RUNTIMES.with(|active| active.borrow().contains(&self.inner.id))
}
pub(crate) fn try_execute<F>(&self, job: F) -> io::Result<()>
where
F: FnOnce() + Send + 'static,
{
let id = self.inner.id;
let wrapped: Job = Box::new(move || {
let _guard = ActiveRuntimeGuard::enter(id);
job();
});
match &self.inner.executor {
Executor::Global => ThreadPool::global().execute(wrapped),
Executor::Dedicated(pool) => pool.execute(wrapped),
Executor::External(executor) => executor.try_execute(wrapped, self.inner.busy_timeout),
}
}
fn new(executor: Executor, busy_timeout: Option<Duration>) -> Self {
static NEXT_RUNTIME_ID: AtomicU64 = AtomicU64::new(1);
Self {
inner: Arc::new(RuntimeInner {
id: NEXT_RUNTIME_ID.fetch_add(1, Ordering::Relaxed),
executor,
busy_timeout,
}),
}
}
}
impl Default for ParallelRuntime {
fn default() -> Self {
Self::global()
}
}
impl fmt::Debug for ParallelRuntime {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
let kind = match &self.inner.executor {
Executor::Global => "global",
Executor::Dedicated(_) => "dedicated",
Executor::External(_) => "external",
};
formatter
.debug_struct("ParallelRuntime")
.field("kind", &kind)
.field("parallelism", &self.parallelism())
.field("busy_timeout", &self.inner.busy_timeout)
.finish()
}
}
impl Clone for RuntimeInner {
fn clone(&self) -> Self {
Self {
id: self.id,
executor: match &self.executor {
Executor::Global => Executor::Global,
Executor::Dedicated(pool) => Executor::Dedicated(Arc::clone(pool)),
Executor::External(executor) => Executor::External(Arc::clone(executor)),
},
busy_timeout: self.busy_timeout,
}
}
}
struct ActiveRuntimeGuard;
impl ActiveRuntimeGuard {
fn enter(id: u64) -> Self {
ACTIVE_RUNTIMES.with(|active| active.borrow_mut().push(id));
Self
}
}
impl Drop for ActiveRuntimeGuard {
fn drop(&mut self) {
ACTIVE_RUNTIMES.with(|active| {
active.borrow_mut().pop();
});
}
}
#[cfg(test)]
mod tests;