use crate::loom::sync::{Arc, Condvar, Mutex};
use crate::loom::thread;
use crate::runtime::blocking::schedule::BlockingSchedule;
use crate::runtime::blocking::sharded::ShardedImpl;
use crate::runtime::blocking::{shutdown, BlockingTask};
use crate::runtime::builder::ThreadNameFn;
use crate::runtime::task::{self, JoinHandle};
use crate::runtime::{AutoBox, Builder, Callback, Handle};
use crate::util::metric_atomics::MetricAtomicUsize;
use crate::util::trace::{blocking_task, SpawnMeta};
use std::collections::{HashMap, VecDeque};
use std::fmt;
use std::io;
use std::sync::atomic::Ordering;
use std::time::Duration;
pub(crate) struct BlockingPool {
spawner: Spawner,
shutdown_rx: shutdown::Receiver,
}
#[derive(Clone)]
pub(crate) struct Spawner {
inner: Arc<Inner>,
}
#[derive(Default)]
pub(crate) struct SpawnerMetrics {
num_threads: MetricAtomicUsize,
num_idle_threads: MetricAtomicUsize,
queue_depth: MetricAtomicUsize,
}
impl SpawnerMetrics {
pub(super) fn num_threads(&self) -> usize {
self.num_threads.load(Ordering::Relaxed)
}
pub(super) fn num_idle_threads(&self) -> usize {
self.num_idle_threads.load(Ordering::Relaxed)
}
cfg_unstable_metrics! {
fn queue_depth(&self) -> usize {
self.queue_depth.load(Ordering::Relaxed)
}
}
fn inc_num_threads(&self) {
self.num_threads.increment();
}
pub(super) fn dec_num_threads(&self) {
self.num_threads.decrement();
}
pub(super) fn inc_num_idle_threads(&self) {
self.num_idle_threads.increment();
}
pub(super) fn dec_num_idle_threads(&self) -> usize {
self.num_idle_threads.decrement()
}
pub(super) fn inc_queue_depth(&self) {
self.queue_depth.increment();
}
pub(super) fn dec_queue_depth(&self) {
self.queue_depth.decrement();
}
}
struct Inner {
inner_impl: InnerImpl,
thread_name: ThreadNameFn,
stack_size: Option<usize>,
after_start: Option<Callback>,
before_stop: Option<Callback>,
thread_cap: usize,
scheduler_threads: usize,
keep_alive: Duration,
metrics: SpawnerMetrics,
}
enum InnerImpl {
Locked(LockedImpl),
Sharded(ShardedImpl),
}
struct LockedImpl {
mutex: Mutex<LockedInner>,
condvar: Condvar,
}
struct LockedInner {
queue: VecDeque<Task>,
num_notify: u32,
thread_mgmt_state: ThreadManagementState,
}
pub(super) type ShutdownHandles = (
Option<thread::JoinHandle<()>>,
HashMap<usize, thread::JoinHandle<()>>,
);
pub(super) struct ThreadManagementState {
pub(super) shutdown: bool,
pub(super) shutdown_tx: Option<shutdown::Sender>,
pub(super) last_exiting_thread: Option<thread::JoinHandle<()>>,
pub(super) worker_threads: HashMap<usize, thread::JoinHandle<()>>,
pub(super) worker_thread_index: usize,
}
impl ThreadManagementState {
pub(super) fn begin_shutdown(&mut self) -> Option<ShutdownHandles> {
if self.shutdown {
return None;
}
self.shutdown = true;
self.shutdown_tx = None;
let last_exited_thread = std::mem::take(&mut self.last_exiting_thread);
let workers = std::mem::take(&mut self.worker_threads);
Some((last_exited_thread, workers))
}
pub(super) fn worker_timed_out(
&mut self,
worker_thread_id: usize,
) -> Option<thread::JoinHandle<()>> {
let my_handle = self.worker_threads.remove(&worker_thread_id);
std::mem::replace(&mut self.last_exiting_thread, my_handle)
}
}
pub(crate) struct Task {
task: task::UnownedTask<BlockingSchedule>,
mandatory: Mandatory,
}
#[derive(PartialEq, Eq)]
pub(crate) enum Mandatory {
#[cfg_attr(any(not(feature = "fs"), target_os = "emscripten"), allow(dead_code))]
Mandatory,
NonMandatory,
}
pub(crate) enum SpawnError {
ShuttingDown,
NoThreads(io::Error),
}
impl From<SpawnError> for io::Error {
fn from(e: SpawnError) -> Self {
match e {
SpawnError::ShuttingDown => io::Error::other("blocking pool shutting down"),
SpawnError::NoThreads(e) => e,
}
}
}
impl Task {
pub(crate) fn new(task: task::UnownedTask<BlockingSchedule>, mandatory: Mandatory) -> Task {
Task { task, mandatory }
}
pub(super) fn shutdown(self) {
self.task.shutdown();
}
pub(super) fn run(self) {
self.task.run();
}
pub(super) fn shutdown_or_run_if_mandatory(self) {
match self.mandatory {
Mandatory::NonMandatory => self.task.shutdown(),
Mandatory::Mandatory => self.task.run(),
}
}
}
const KEEP_ALIVE: Duration = Duration::from_secs(10);
#[track_caller]
#[cfg_attr(target_os = "wasi", allow(dead_code))]
pub(crate) fn spawn_blocking<F, R>(func: F) -> JoinHandle<R>
where
F: FnOnce() -> R + Send + 'static,
R: Send + 'static,
{
let rt = Handle::current();
rt.spawn_blocking(func)
}
cfg_io_blocking! {
#[cfg_attr(any(
all(loom, not(test)), // the function is covered by loom tests
test,
target_os = "emscripten", // fs and io-std use the inline shim
), allow(dead_code))]
pub(crate) fn spawn_mandatory_blocking<F, R>(func: F) -> Option<JoinHandle<R>>
where
F: FnOnce() -> R + Send + 'static,
R: Send + 'static,
{
let rt = Handle::current();
rt.inner.blocking_spawner().spawn_mandatory_blocking(&rt, func)
}
}
impl BlockingPool {
pub(crate) fn new(
builder: &Builder,
thread_cap: usize,
scheduler_threads: usize,
) -> BlockingPool {
let (shutdown_tx, shutdown_rx) = shutdown::channel();
let keep_alive = builder.keep_alive.unwrap_or(KEEP_ALIVE);
let thread_mgmt_state = ThreadManagementState {
shutdown: false,
shutdown_tx: Some(shutdown_tx),
last_exiting_thread: None,
worker_threads: HashMap::new(),
worker_thread_index: 0,
};
let inner_impl = if builder.sharded_blocking_queue {
InnerImpl::Sharded(ShardedImpl::new(thread_mgmt_state))
} else {
InnerImpl::Locked(LockedImpl::new(thread_mgmt_state))
};
BlockingPool {
spawner: Spawner {
inner: Arc::new(Inner {
inner_impl,
thread_name: builder.thread_name.clone(),
stack_size: builder.thread_stack_size,
after_start: builder.after_start.clone(),
before_stop: builder.before_stop.clone(),
thread_cap,
scheduler_threads,
keep_alive,
metrics: SpawnerMetrics::default(),
}),
},
shutdown_rx,
}
}
pub(crate) fn spawner(&self) -> &Spawner {
&self.spawner
}
pub(crate) fn shutdown(&mut self, timeout: Option<Duration>) {
let inner = &self.spawner.inner;
let (last_exited_thread, workers) = match inner.inner_impl.begin_shutdown(&inner.metrics) {
Some(x) => x,
None => return,
};
if self.shutdown_rx.wait(timeout) {
let _ = last_exited_thread.map(thread::JoinHandle::join);
#[cfg(loom)]
let workers: Vec<(usize, thread::JoinHandle<()>)> = {
let mut workers: Vec<_> = workers.into_iter().collect();
workers.sort_by_key(|(id, _)| *id);
workers
};
for (_id, handle) in workers {
let _ = handle.join();
}
}
}
}
impl Drop for BlockingPool {
fn drop(&mut self) {
self.shutdown(None);
}
}
impl fmt::Debug for BlockingPool {
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt.debug_struct("BlockingPool").finish()
}
}
impl Spawner {
#[track_caller]
pub(crate) fn spawn_blocking<F, R>(&self, rt: &Handle, func: F) -> JoinHandle<R>
where
F: FnOnce() -> R + Send + 'static,
R: Send + 'static,
{
let fn_size = std::mem::size_of::<F>();
let (join_handle, spawn_result) = if AutoBox::<F>::SHOULD_BOX {
self.spawn_blocking_inner(
Box::new(func),
Mandatory::NonMandatory,
SpawnMeta::new_unnamed(fn_size),
rt,
)
} else {
self.spawn_blocking_inner(
func,
Mandatory::NonMandatory,
SpawnMeta::new_unnamed(fn_size),
rt,
)
};
match spawn_result {
Ok(()) => join_handle,
Err(SpawnError::ShuttingDown) => join_handle,
Err(SpawnError::NoThreads(e)) => {
panic!("OS can't spawn worker thread: {e}")
}
}
}
cfg_io_blocking! {
#[track_caller]
#[cfg_attr(any(
all(loom, not(test)), // the function is covered by loom tests
test,
target_os = "emscripten", // fs and io-std use the inline shim
), allow(dead_code))]
pub(crate) fn spawn_mandatory_blocking<F, R>(&self, rt: &Handle, func: F) -> Option<JoinHandle<R>>
where
F: FnOnce() -> R + Send + 'static,
R: Send + 'static,
{
let fn_size = std::mem::size_of::<F>();
let (join_handle, spawn_result) = if AutoBox::<F>::SHOULD_BOX {
self.spawn_blocking_inner(
Box::new(func),
Mandatory::Mandatory,
SpawnMeta::new_unnamed(fn_size),
rt,
)
} else {
self.spawn_blocking_inner(
func,
Mandatory::Mandatory,
SpawnMeta::new_unnamed(fn_size),
rt,
)
};
if spawn_result.is_ok() {
Some(join_handle)
} else {
None
}
}
}
#[track_caller]
pub(crate) fn spawn_blocking_inner<F, R>(
&self,
func: F,
is_mandatory: Mandatory,
spawn_meta: SpawnMeta<'_>,
rt: &Handle,
) -> (JoinHandle<R>, Result<(), SpawnError>)
where
F: FnOnce() -> R + Send + 'static,
R: Send + 'static,
{
let id = task::Id::next();
let fut =
blocking_task::<F, BlockingTask<F>>(BlockingTask::new(func), spawn_meta, id.as_u64());
let (task, handle) = task::unowned(
fut,
BlockingSchedule::new(rt),
id,
task::SpawnLocation::capture(),
);
let spawned = self.spawn_task(Task::new(task, is_mandatory), rt);
(handle, spawned)
}
pub(crate) fn num_blocking_threads(&self) -> usize {
self.inner
.metrics
.num_threads()
.saturating_sub(self.inner.scheduler_threads)
}
fn spawn_task(&self, task: Task, rt: &Handle) -> Result<(), SpawnError> {
self.inner
.inner_impl
.spawn_task(task, &self.inner.metrics, |thread_mgmt_state| {
if self.inner.metrics.num_threads() == self.inner.thread_cap {
return Ok(());
}
assert!(thread_mgmt_state.shutdown_tx.is_some());
let shutdown_tx = thread_mgmt_state.shutdown_tx.clone();
if let Some(shutdown_tx) = shutdown_tx {
let id = thread_mgmt_state.worker_thread_index;
match self.spawn_thread(shutdown_tx, rt, id) {
Ok(handle) => {
self.inner.metrics.inc_num_threads();
thread_mgmt_state.worker_thread_index += 1;
thread_mgmt_state.worker_threads.insert(id, handle);
}
Err(ref e)
if is_temporary_os_thread_error(e)
&& self.num_blocking_threads() > 0 =>
{
}
Err(e) => {
return Err(SpawnError::NoThreads(e));
}
}
}
Ok(())
})
}
fn spawn_thread(
&self,
shutdown_tx: shutdown::Sender,
rt: &Handle,
id: usize,
) -> io::Result<thread::JoinHandle<()>> {
let mut builder = thread::Builder::new().name((self.inner.thread_name)());
if let Some(stack_size) = self.inner.stack_size {
builder = builder.stack_size(stack_size);
}
let rt = rt.clone();
builder.spawn(move || {
let _enter = rt.enter();
rt.inner.blocking_spawner().inner.run(id);
drop(shutdown_tx);
})
}
}
cfg_unstable_metrics! {
impl Spawner {
pub(crate) fn num_idle_threads(&self) -> usize {
self.inner.metrics.num_idle_threads()
}
pub(crate) fn queue_depth(&self) -> usize {
self.inner.metrics.queue_depth()
}
}
}
impl InnerImpl {
fn spawn_task<F>(
&self,
task: Task,
metrics: &SpawnerMetrics,
on_no_idle: F,
) -> Result<(), SpawnError>
where
F: FnOnce(&mut ThreadManagementState) -> Result<(), SpawnError>,
{
match self {
InnerImpl::Locked(l) => l.spawn_task(task, metrics, on_no_idle),
InnerImpl::Sharded(s) => s.spawn_task(task, metrics, on_no_idle),
}
}
fn run_worker(
&self,
metrics: &SpawnerMetrics,
keep_alive: Duration,
worker_thread_id: usize,
) -> Option<thread::JoinHandle<()>> {
match self {
InnerImpl::Locked(l) => l.run_worker(metrics, keep_alive, worker_thread_id),
InnerImpl::Sharded(s) => s.run_worker(metrics, keep_alive, worker_thread_id),
}
}
fn begin_shutdown(&self, metrics: &SpawnerMetrics) -> Option<ShutdownHandles> {
match self {
InnerImpl::Locked(l) => l.begin_shutdown(),
InnerImpl::Sharded(s) => s.begin_shutdown(metrics),
}
}
}
impl LockedImpl {
fn new(thread_mgmt_state: ThreadManagementState) -> LockedImpl {
LockedImpl {
mutex: Mutex::new(LockedInner {
queue: VecDeque::new(),
num_notify: 0,
thread_mgmt_state,
}),
condvar: Condvar::new(),
}
}
fn spawn_task<F>(
&self,
task: Task,
metrics: &SpawnerMetrics,
on_no_idle: F,
) -> Result<(), SpawnError>
where
F: FnOnce(&mut ThreadManagementState) -> Result<(), SpawnError>,
{
let mut locked = self.mutex.lock();
if locked.thread_mgmt_state.shutdown {
drop(locked);
task.task.shutdown();
return Err(SpawnError::ShuttingDown);
}
locked.queue.push_back(task);
metrics.inc_queue_depth();
if metrics.num_idle_threads() == 0 {
on_no_idle(&mut locked.thread_mgmt_state)?;
} else {
metrics.dec_num_idle_threads();
locked.num_notify += 1;
self.condvar.notify_one();
}
Ok(())
}
fn run_worker(
&self,
metrics: &SpawnerMetrics,
keep_alive: Duration,
worker_thread_id: usize,
) -> Option<thread::JoinHandle<()>> {
let mut locked = self.mutex.lock();
let mut join_on_thread = None;
let mut is_counted_idle;
'main: loop {
while let Some(task) = locked.queue.pop_front() {
metrics.dec_queue_depth();
drop(locked);
task.run();
locked = self.mutex.lock();
}
metrics.inc_num_idle_threads();
is_counted_idle = true;
while !locked.thread_mgmt_state.shutdown {
let lock_result = self.condvar.wait_timeout(locked, keep_alive).unwrap();
locked = lock_result.0;
let timeout_result = lock_result.1;
if locked.num_notify != 0 {
locked.num_notify -= 1;
is_counted_idle = false;
break;
}
if !locked.thread_mgmt_state.shutdown && timeout_result.timed_out() {
join_on_thread = locked.thread_mgmt_state.worker_timed_out(worker_thread_id);
break 'main;
}
}
if locked.thread_mgmt_state.shutdown {
while let Some(task) = locked.queue.pop_front() {
metrics.dec_queue_depth();
drop(locked);
task.shutdown_or_run_if_mandatory();
locked = self.mutex.lock();
}
break;
}
}
metrics.dec_num_threads();
if is_counted_idle {
let prev_idle = metrics.dec_num_idle_threads();
assert_ne!(
prev_idle, 0,
"`num_idle_threads` underflowed on thread exit"
);
}
if locked.thread_mgmt_state.shutdown && metrics.num_threads() == 0 {
self.condvar.notify_one();
}
drop(locked);
join_on_thread
}
fn begin_shutdown(&self) -> Option<ShutdownHandles> {
let mut locked = self.mutex.lock();
let handles = locked.thread_mgmt_state.begin_shutdown()?;
self.condvar.notify_all();
Some(handles)
}
}
#[inline]
fn is_temporary_os_thread_error(error: &io::Error) -> bool {
matches!(error.kind(), io::ErrorKind::WouldBlock)
}
impl Inner {
fn run(&self, worker_thread_id: usize) {
if let Some(f) = &self.after_start {
f();
}
let join_on_thread =
self.inner_impl
.run_worker(&self.metrics, self.keep_alive, worker_thread_id);
if let Some(f) = &self.before_stop {
f();
}
if let Some(handle) = join_on_thread {
let _ = handle.join();
}
}
}
impl fmt::Debug for Spawner {
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt.debug_struct("blocking::Spawner").finish()
}
}