use std::any::Any;
use std::collections::VecDeque;
use std::fmt;
use std::future::Future;
use std::io;
use std::panic::{AssertUnwindSafe, resume_unwind};
use std::pin::Pin;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Condvar, Mutex, MutexGuard, OnceLock};
use std::task::{Context, Poll, Wake, Waker};
use std::thread::{self, Thread};
use std::time::Duration;
struct ThreadWaker {
thread: Thread,
notified: AtomicBool,
}
impl Wake for ThreadWaker {
fn wake(self: Arc<Self>) {
self.wake_by_ref();
}
fn wake_by_ref(self: &Arc<Self>) {
self.notified.store(true, Ordering::Release);
self.thread.unpark();
}
}
pub fn block_on<F: Future>(future: F) -> F::Output {
let mut future = std::pin::pin!(future);
let signal = Arc::new(ThreadWaker {
thread: thread::current(),
notified: AtomicBool::new(false),
});
let waker = Waker::from(Arc::clone(&signal));
let mut cx = Context::from_waker(&waker);
loop {
match future.as_mut().poll(&mut cx) {
Poll::Ready(val) => return val,
Poll::Pending => {
while !signal.notified.swap(false, Ordering::Acquire) {
thread::park();
}
}
}
}
}
pub async fn join_all<F: Future>(futures: impl IntoIterator<Item = F>) -> Vec<F::Output> {
join_all_boxed(futures.into_iter().map(Box::pin)).await
}
pub async fn join_all_boxed<F: Future + ?Sized>(
futures: impl IntoIterator<Item = Pin<Box<F>>>,
) -> Vec<F::Output> {
let mut pending: Vec<Option<Pin<Box<F>>>> = futures.into_iter().map(Some).collect();
let count = pending.len();
let mut done: Vec<Option<F::Output>> = (0..count).map(|_| None).collect();
let ready = Arc::new(ReadySet {
inner: Mutex::new(ReadyInner {
order: (0..count).collect(),
queued: vec![true; count],
group: None,
}),
});
let wakers: Vec<Waker> = (0..count)
.map(|index| {
Waker::from(Arc::new(ChildWaker {
index,
ready: Arc::clone(&ready),
}))
})
.collect();
let mut remaining = count;
std::future::poll_fn(move |cx| {
let woken = {
let mut inner = lock(&ready.inner);
let replace = inner
.group
.as_ref()
.is_none_or(|existing| !existing.will_wake(cx.waker()));
if replace {
inner.group = Some(cx.waker().clone());
}
std::mem::take(&mut inner.order)
};
for index in woken {
if let Some(flag) = lock(&ready.inner).queued.get_mut(index) {
*flag = false;
}
let (Some(slot), Some(output), Some(waker)) = (
pending.get_mut(index),
done.get_mut(index),
wakers.get(index),
) else {
continue;
};
let Some(future) = slot.as_mut() else {
continue;
};
if let Poll::Ready(value) = future.as_mut().poll(&mut Context::from_waker(waker)) {
*output = Some(value);
*slot = None;
remaining = remaining.saturating_sub(1);
}
}
if remaining == 0 {
Poll::Ready(done.drain(..).flatten().collect())
} else {
Poll::Pending
}
})
.await
}
struct ReadySet {
inner: Mutex<ReadyInner>,
}
struct ReadyInner {
order: Vec<usize>,
queued: Vec<bool>,
group: Option<Waker>,
}
struct ChildWaker {
index: usize,
ready: Arc<ReadySet>,
}
impl Wake for ChildWaker {
fn wake(self: Arc<Self>) {
self.wake_by_ref();
}
fn wake_by_ref(self: &Arc<Self>) {
let group = {
let mut inner = lock(&self.ready.inner);
let newly = inner
.queued
.get_mut(self.index)
.is_some_and(|flag| !std::mem::replace(flag, true));
if newly {
inner.order.push(self.index);
}
inner.group.clone()
};
if let Some(group) = group {
group.wake();
}
}
}
fn lock<T>(mutex: &Mutex<T>) -> MutexGuard<'_, T> {
mutex
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
}
enum Job<T> {
Running,
Done(T),
Panicked(Box<dyn Any + Send>),
Taken,
}
struct Shared<T> {
job: Job<T>,
waker: Option<Waker>,
}
pub struct JoinHandle<T> {
shared: Arc<Mutex<Shared<T>>>,
}
impl<T> Future for JoinHandle<T> {
type Output = T;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<T> {
let mut state = lock(&self.shared);
match std::mem::replace(&mut state.job, Job::Taken) {
Job::Done(value) => Poll::Ready(value),
Job::Panicked(payload) => {
drop(state);
resume_unwind(payload)
}
Job::Taken => resume_unwind(Box::new(
"spawn_blocking JoinHandle polled after completion",
)),
Job::Running => {
state.job = Job::Running;
let replace = match state.waker.as_ref() {
Some(existing) => !existing.will_wake(cx.waker()),
None => true,
};
if replace {
state.waker = Some(cx.waker().clone());
}
Poll::Pending
}
}
}
}
pub fn spawn_blocking<F, T>(job: F) -> JoinHandle<T>
where
F: FnOnce() -> T + Send + 'static,
T: Send + 'static,
{
spawn_blocking_on(pool(), job)
}
fn spawn_blocking_on<F, T>(pool: &'static Pool, job: F) -> JoinHandle<T>
where
F: FnOnce() -> T + Send + 'static,
T: Send + 'static,
{
let (handle, work) = prepare(job);
if let Err(error) = pool.submit(work, None) {
lock(&handle.shared).job = Job::Panicked(Box::new(error));
}
handle
}
pub fn try_spawn_blocking<F, T>(job: F) -> Result<JoinHandle<T>, SpawnError>
where
F: FnOnce() -> T + Send + 'static,
T: Send + 'static,
{
let (handle, work) = prepare(job);
pool().submit(work, Some(MAX_QUEUED_BLOCKING_JOBS))?;
Ok(handle)
}
fn prepare<F, T>(job: F) -> (JoinHandle<T>, Work)
where
F: FnOnce() -> T + Send + 'static,
T: Send + 'static,
{
let shared = Arc::new(Mutex::new(Shared {
job: Job::Running,
waker: None,
}));
let worker = Arc::clone(&shared);
let work: Work = Box::new(move || {
let outcome = std::panic::catch_unwind(AssertUnwindSafe(job));
let mut state = lock(&worker);
state.job = match outcome {
Ok(value) => Job::Done(value),
Err(payload) => Job::Panicked(payload),
};
if let Some(waker) = state.waker.take() {
drop(state);
waker.wake();
}
});
(JoinHandle { shared }, work)
}
const MAX_BLOCKING_THREADS: usize = 512;
const MAX_QUEUED_BLOCKING_JOBS: usize = 16_384;
const BLOCKING_KEEP_ALIVE: Duration = Duration::from_secs(10);
#[derive(Debug)]
#[non_exhaustive]
pub enum SpawnError {
AtCapacity {
threads: usize,
queued: usize,
},
Os(io::Error),
}
impl fmt::Display for SpawnError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match *self {
Self::AtCapacity { threads, queued } => write!(
formatter,
"the blocking pool is at capacity: {threads} threads busy and {queued} jobs waiting"
),
Self::Os(ref error) => write!(formatter, "could not start a blocking thread: {error}"),
}
}
}
impl std::error::Error for SpawnError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match *self {
Self::Os(ref error) => Some(error),
Self::AtCapacity { .. } => None,
}
}
}
type Work = Box<dyn FnOnce() + Send>;
struct Pool {
state: Mutex<PoolState>,
work_ready: Condvar,
ceiling: usize,
start: fn(&'static Pool, Handoff) -> io::Result<()>,
}
struct PoolState<W = Work> {
queue: VecDeque<W>,
live: usize,
idle: usize,
wakeups: usize,
}
#[derive(Debug, PartialEq, Eq)]
enum Admitted<W> {
Woke,
Start(W),
Queued,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Woken {
Resume,
Exit,
Wait,
}
impl<W> PoolState<W> {
const fn new() -> Self {
Self {
queue: VecDeque::new(),
live: 0,
idle: 0,
wakeups: 0,
}
}
fn admit(
&mut self,
work: W,
queue_limit: Option<usize>,
ceiling: usize,
) -> Result<Admitted<W>, W> {
if queue_limit.is_some_and(|limit| self.queue.len() >= limit) {
#[cfg(feature = "trace")]
crate::trace::debug!(queued = self.queue.len(), limit = ?queue_limit, "admit: the wait queue is at its bound");
return Err(work);
}
if self.idle == 0 && self.live < ceiling {
return Ok(Admitted::Start(work));
}
self.queue.push_back(work);
if self.idle > 0 {
self.idle = self.idle.saturating_sub(1);
self.wakeups = self.wakeups.saturating_add(1);
return Ok(Admitted::Woke);
}
Ok(Admitted::Queued)
}
const fn started(&mut self) {
self.live = self.live.saturating_add(1);
}
fn start_failed(&mut self, work: W) -> Option<W> {
if self.live > 0 {
self.queue.push_back(work);
return None;
}
Some(work)
}
fn take(&mut self) -> Option<W> {
self.queue.pop_front()
}
const fn park(&mut self) {
self.idle = self.idle.saturating_add(1);
}
const fn woken(&mut self, timed_out: bool) -> Woken {
if self.wakeups > 0 {
self.wakeups = self.wakeups.saturating_sub(1);
return Woken::Resume;
}
if timed_out {
self.idle = self.idle.saturating_sub(1);
self.live = self.live.saturating_sub(1);
return Woken::Exit;
}
Woken::Wait
}
}
fn pool() -> &'static Pool {
static POOL: OnceLock<Pool> = OnceLock::new();
POOL.get_or_init(|| Pool::new(MAX_BLOCKING_THREADS, start_os_thread))
}
fn start_os_thread(pool: &'static Pool, first: Handoff) -> io::Result<()> {
thread::Builder::new()
.name("lgwks-blocking".into())
.spawn(move || {
let work = lock(&first).take();
drop(first);
if let Some(work) = work {
work();
}
pool.run();
})
.map(drop)
}
type Handoff = Arc<Mutex<Option<Work>>>;
impl Pool {
const fn new(ceiling: usize, start: fn(&'static Self, Handoff) -> io::Result<()>) -> Self {
Self {
state: Mutex::new(PoolState::new()),
work_ready: Condvar::new(),
ceiling,
start,
}
}
fn submit(&'static self, work: Work, queue_limit: Option<usize>) -> Result<(), SpawnError> {
let mut state = lock(&self.state);
let admitted = match state.admit(work, queue_limit, self.ceiling) {
Ok(admitted) => admitted,
Err(_refused) => {
let refusal = Err(SpawnError::AtCapacity {
threads: self.ceiling,
queued: queue_limit.unwrap_or(usize::MAX),
});
#[cfg(feature = "trace")]
crate::trace::debug!(error = ?refusal.as_ref().err(), "submit: the blocking pool refused a job");
return refusal;
}
};
match admitted {
Admitted::Woke => {
self.work_ready.notify_one();
Ok(())
}
Admitted::Queued => Ok(()),
Admitted::Start(work) => {
let first = Arc::new(Mutex::new(Some(work)));
match (self.start)(self, Arc::clone(&first)) {
Ok(()) => {
state.started();
Ok(())
}
Err(error) => {
let Some(work) = lock(&first).take() else {
return Ok(());
};
match state.start_failed(work) {
None => Ok(()),
Some(_unrun) => {
let refusal = Err(SpawnError::Os(error));
#[cfg(feature = "trace")]
crate::trace::debug!(error = ?refusal.as_ref().err(), "submit: no blocking thread could be started");
refusal
}
}
}
}
}
}
}
fn run(&self) {
let mut state = lock(&self.state);
loop {
if let Some(work) = state.take() {
drop(state);
work();
state = lock(&self.state);
continue;
}
state.park();
loop {
let (guard, waited) = self
.work_ready
.wait_timeout(state, BLOCKING_KEEP_ALIVE)
.unwrap_or_else(std::sync::PoisonError::into_inner);
state = guard;
match state.woken(waited.timed_out()) {
Woken::Resume => break,
Woken::Exit => return,
Woken::Wait => {}
}
}
}
}
}
impl<T> core::fmt::Debug for JoinHandle<T> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
let state = lock(&self.shared);
let state = match state.job {
Job::Running => "running",
Job::Done(_) => "done",
Job::Panicked(_) => "panicked",
Job::Taken => "taken",
};
f.debug_struct("JoinHandle").field("state", &state).finish()
}
}
#[cfg(test)]
#[path = "sim_task_pool.rs"]
mod sim_pool;
#[cfg(test)]
#[expect(
clippy::disallowed_methods,
reason = "tests of a thread-parking executor must spawn a waker thread and sleep to let the driver park"
)]
mod tests {
use super::*;
use std::sync::atomic::{AtomicUsize, Ordering as AtomicOrdering};
use std::time::Duration;
#[test]
fn immediate_future_returns_value() {
let result = block_on(async { 42 });
assert_eq!(result, 42);
}
#[test]
fn yields_and_resumes() {
async fn step() -> String {
let first_part = async { "hello" }.await;
let second_part = async { "world" }.await;
format!("{first_part} {second_part}")
}
assert_eq!(block_on(step()), "hello world");
}
struct DeferredValue {
value: Arc<Mutex<Option<i32>>>,
waker_slot: Arc<Mutex<Option<Waker>>>,
}
impl Future for DeferredValue {
type Output = i32;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
if let Some(value) = lock(&self.value).take() {
return Poll::Ready(value);
}
*lock(&self.waker_slot) = Some(cx.waker().clone());
Poll::Pending
}
}
#[test]
fn threaded_waker_unparks() {
let value: Arc<Mutex<Option<i32>>> = Arc::new(Mutex::new(None));
let waker_slot: Arc<Mutex<Option<Waker>>> = Arc::new(Mutex::new(None));
let sender_value = Arc::clone(&value);
let sender_slot = Arc::clone(&waker_slot);
thread::scope(|scope| {
scope.spawn(move || {
thread::sleep(Duration::from_millis(5));
*lock(&sender_value) = Some(100);
if let Some(waker) = lock(&sender_slot).take() {
waker.wake();
}
});
let result = block_on(DeferredValue { value, waker_slot });
assert_eq!(result, 100);
});
}
#[test]
fn join_all_empty_resolves_immediately() {
let output: Vec<u8> = block_on(join_all(std::iter::empty::<std::future::Ready<u8>>()));
assert!(output.is_empty());
}
#[test]
fn join_all_preserves_input_order_across_completion_order() {
let output = block_on(join_all(vec![
spawn_blocking(|| {
thread::sleep(Duration::from_millis(30));
1u32
}),
spawn_blocking(|| 2u32),
spawn_blocking(|| {
thread::sleep(Duration::from_millis(10));
3u32
}),
]));
assert_eq!(output, vec![1, 2, 3]);
}
#[test]
fn join_all_runs_children_concurrently() {
let barrier = Arc::new(std::sync::Barrier::new(2));
let handles: Vec<_> = (0..2)
.map(|_| {
let barrier = Arc::clone(&barrier);
spawn_blocking(move || {
barrier.wait();
7u32
})
})
.collect();
let output = block_on(join_all(handles));
assert_eq!(output, vec![7, 7]);
}
struct CountPolls {
polls: Arc<AtomicUsize>,
}
impl Future for CountPolls {
type Output = usize;
fn poll(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<usize> {
let n = self
.polls
.fetch_add(1, AtomicOrdering::SeqCst)
.saturating_add(1);
Poll::Ready(n)
}
}
struct PendingThenReady {
polls: Arc<AtomicUsize>,
waker_slot: Arc<Mutex<Option<Waker>>>,
}
impl Future for PendingThenReady {
type Output = usize;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<usize> {
let n = self
.polls
.fetch_add(1, AtomicOrdering::SeqCst)
.saturating_add(1);
if n > 1 {
return Poll::Ready(n);
}
*lock(&self.waker_slot) = Some(cx.waker().clone());
let slot = Arc::clone(&self.waker_slot);
thread::spawn(move || {
thread::sleep(Duration::from_millis(5));
if let Some(waker) = lock(&slot).take() {
waker.wake();
}
});
Poll::Pending
}
}
#[test]
fn join_all_never_repolls_a_completed_future() {
let fast_polls = Arc::new(AtomicUsize::new(0));
let slow_polls = Arc::new(AtomicUsize::new(0));
let fast: Pin<Box<dyn Future<Output = usize>>> = Box::pin(CountPolls {
polls: Arc::clone(&fast_polls),
});
let slow: Pin<Box<dyn Future<Output = usize>>> = Box::pin(PendingThenReady {
polls: Arc::clone(&slow_polls),
waker_slot: Arc::new(Mutex::new(None)),
});
let output = block_on(join_all(vec![fast, slow]));
assert_eq!(output, vec![1, 2]);
assert_eq!(fast_polls.load(AtomicOrdering::SeqCst), 1);
assert_eq!(slow_polls.load(AtomicOrdering::SeqCst), 2);
}
#[test]
fn join_all_boxed_matches_join_all_and_accepts_boxes() {
let plain = block_on(join_all(vec![std::future::ready(1), std::future::ready(2)]));
let boxed: Vec<Pin<Box<dyn Future<Output = i32>>>> = vec![
Box::pin(std::future::ready(1)),
Box::pin(std::future::ready(2)),
];
assert_eq!(plain, vec![1, 2]);
assert_eq!(block_on(join_all_boxed(boxed)), vec![1, 2]);
let empty: Vec<Pin<Box<dyn Future<Output = i32>>>> = Vec::new();
assert_eq!(block_on(join_all_boxed(empty)), Vec::<i32>::new());
}
#[derive(Default)]
struct Latch {
open: AtomicBool,
parked: Mutex<Vec<Waker>>,
}
struct Waiter {
latch: Arc<Latch>,
polls: Arc<AtomicUsize>,
}
impl Future for Waiter {
type Output = ();
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
self.polls.fetch_add(1, AtomicOrdering::SeqCst);
if self.latch.open.load(AtomicOrdering::SeqCst) {
return Poll::Ready(());
}
lock(&self.latch.parked).push(cx.waker().clone());
Poll::Pending
}
}
struct Busy {
wakes: usize,
latch: Arc<Latch>,
polls: Arc<AtomicUsize>,
}
impl Future for Busy {
type Output = ();
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
self.polls.fetch_add(1, AtomicOrdering::SeqCst);
if self.wakes == 0 {
self.latch.open.store(true, AtomicOrdering::SeqCst);
for waker in lock(&self.latch.parked).drain(..) {
waker.wake();
}
return Poll::Ready(());
}
self.wakes = self.wakes.saturating_sub(1);
cx.waker().wake_by_ref();
Poll::Pending
}
}
#[test]
fn a_wake_polls_only_the_child_that_woke() {
for (waiters, wakes) in [
(1usize, 0usize),
(10, 3),
(1_000, 50),
(10_000, 20),
(100_000, 5),
] {
let latch = Arc::new(Latch::default());
let polls = Arc::new(AtomicUsize::new(0));
let mut children: Vec<Pin<Box<dyn Future<Output = ()>>>> = (0..waiters)
.map(|_| -> Pin<Box<dyn Future<Output = ()>>> {
Box::pin(Waiter {
latch: Arc::clone(&latch),
polls: Arc::clone(&polls),
})
})
.collect();
children.push(Box::pin(Busy {
wakes,
latch: Arc::clone(&latch),
polls: Arc::clone(&polls),
}));
let output = block_on(join_all_boxed(children));
assert_eq!(output.len(), waiters.saturating_add(1));
assert_eq!(
polls.load(AtomicOrdering::SeqCst),
wakes
.saturating_add(1)
.saturating_add(waiters.saturating_mul(2)),
"{waiters} waiters beside a child waking {wakes} times"
);
}
}
#[derive(Default)]
struct Gate {
open: Mutex<bool>,
opened: std::sync::Condvar,
inside: AtomicUsize,
peak: AtomicUsize,
}
impl Gate {
fn pass(&self) {
let now = self
.inside
.fetch_add(1, AtomicOrdering::SeqCst)
.saturating_add(1);
self.peak.fetch_max(now, AtomicOrdering::SeqCst);
let mut open = lock(&self.open);
while !*open {
open = self
.opened
.wait(open)
.unwrap_or_else(std::sync::PoisonError::into_inner);
}
drop(open);
self.inside.fetch_sub(1, AtomicOrdering::SeqCst);
}
fn release(&self) {
*lock(&self.open) = true;
self.opened.notify_all();
}
}
#[test]
fn no_more_than_the_ceiling_run_at_once_and_every_job_completes() -> Result<(), SpawnError> {
for jobs in [100usize, 1_000, 10_000] {
let gate = Arc::new(Gate::default());
let mut handles = Vec::with_capacity(jobs);
for index in 0..jobs {
let gate = Arc::clone(&gate);
handles.push(try_spawn_blocking(move || {
gate.pass();
index
})?);
}
thread::sleep(Duration::from_millis(200));
gate.release();
let output = block_on(join_all(handles));
assert_eq!(output, (0..jobs).collect::<Vec<_>>(), "{jobs} jobs");
let peak = gate.peak.load(AtomicOrdering::SeqCst);
assert!(
peak <= MAX_BLOCKING_THREADS,
"{jobs} jobs: {peak} ran at once, over the ceiling"
);
assert!(
peak >= jobs.min(MAX_BLOCKING_THREADS).min(64),
"{jobs} jobs: only {peak} ran at once; the pool is not running in parallel"
);
}
Ok(())
}
#[test]
fn past_the_queue_bound_a_job_is_refused_with_a_typed_reason() {
const OFFERED: usize = 100_000;
let gate = Arc::new(Gate::default());
let mut accepted = Vec::new();
let mut refused = 0usize;
for _ in 0..OFFERED {
let gate = Arc::clone(&gate);
match try_spawn_blocking(move || gate.pass()) {
Ok(handle) => accepted.push(handle),
Err(error) => {
assert!(
matches!(
error,
SpawnError::AtCapacity {
threads: MAX_BLOCKING_THREADS,
queued: MAX_QUEUED_BLOCKING_JOBS,
}
),
"refused for the wrong reason: {error}"
);
refused = refused.saturating_add(1);
}
}
}
gate.release();
let ceiling = MAX_QUEUED_BLOCKING_JOBS.saturating_add(MAX_BLOCKING_THREADS);
assert!(
(MAX_QUEUED_BLOCKING_JOBS..=ceiling).contains(&accepted.len()),
"{} accepted, outside [{MAX_QUEUED_BLOCKING_JOBS}, {ceiling}]",
accepted.len()
);
assert_eq!(accepted.len().saturating_add(refused), OFFERED);
let completed = block_on(join_all(accepted)).len();
assert!(
completed >= MAX_QUEUED_BLOCKING_JOBS,
"every accepted job ran"
);
assert!(gate.peak.load(AtomicOrdering::SeqCst) <= MAX_BLOCKING_THREADS);
}
#[test]
fn a_panicking_job_does_not_cost_the_pool_its_thread() {
for _ in 0..4 {
let handle = spawn_blocking(|| -> u32 {
assert_eq!(1, 2, "deliberate");
0
});
let caught = std::panic::catch_unwind(AssertUnwindSafe(|| block_on(handle)));
assert!(caught.is_err(), "the panic reaches the awaiter");
}
assert_eq!(block_on(spawn_blocking(|| 5u32)), 5);
}
fn refuse_to_start(_pool: &'static Pool, _first: Handoff) -> io::Result<()> {
Err(io::Error::new(
io::ErrorKind::OutOfMemory,
"injected: no thread",
))
}
fn test_pool(
ceiling: usize,
start: fn(&'static Pool, Handoff) -> io::Result<()>,
) -> &'static Pool {
Box::leak(Box::new(Pool::new(ceiling, start)))
}
#[test]
fn a_thread_that_cannot_start_refuses_the_job_and_it_never_runs() {
let refusing = test_pool(4, refuse_to_start);
let ran = Arc::new(AtomicBool::new(false));
let witness = Arc::clone(&ran);
let (_handle, work) = prepare(move || witness.store(true, Ordering::SeqCst));
match refusing.submit(work, Some(8)) {
Err(SpawnError::Os(error)) => assert_eq!(
(error.kind(), error.to_string()),
(io::ErrorKind::OutOfMemory, "injected: no thread".to_owned()),
"the OS's own error is the source"
),
other => unreachable!("expected an Os refusal, got {other:?}"),
}
let state = lock(&refusing.state);
assert!(
state.queue.is_empty() && state.live == 0,
"a refused job leaves nothing queued and no thread counted"
);
drop(state);
assert!(!ran.load(Ordering::SeqCst), "a refused job never runs");
}
#[test]
fn spawn_blocking_reports_a_refusal_to_its_awaiter_as_the_job_failing() {
let refusing = test_pool(4, refuse_to_start);
let handle = spawn_blocking_on(refusing, || 7u32);
let unwound = std::panic::catch_unwind(AssertUnwindSafe(|| block_on(handle)));
let payload = unwound
.err()
.map(|payload| payload.downcast::<SpawnError>());
assert!(
matches!(payload, Some(Ok(ref error)) if matches!(**error, SpawnError::Os(_))),
"the awaiter unwinds with the SpawnError itself"
);
}
#[test]
fn a_failed_start_beside_a_live_thread_leaves_the_job_to_that_thread() {
fn first_only(pool: &'static Pool, first: Handoff) -> io::Result<()> {
static STARTS: AtomicUsize = AtomicUsize::new(0);
if STARTS.fetch_add(1, AtomicOrdering::SeqCst) == 0 {
start_os_thread(pool, first)
} else {
refuse_to_start(pool, first)
}
}
let single = test_pool(4, first_only);
let gate = Arc::new(Gate::default());
let held = Arc::clone(&gate);
let first = spawn_blocking_on(single, move || {
held.pass();
thread::current().id()
});
let second = spawn_blocking_on(single, || thread::current().id());
assert_eq!(lock(&single.state).live, 1, "the second start was refused");
gate.release();
let (first, second) = (block_on(first), block_on(second));
assert_eq!(
first, second,
"the live thread ran the job its sibling could not start"
);
}
#[test]
fn spawn_blocking_returns_value() {
assert_eq!(block_on(spawn_blocking(|| 99u32)), 99);
}
#[test]
fn spawn_blocking_result_ready_before_first_poll() {
let handle = spawn_blocking(|| 1234u32);
thread::sleep(Duration::from_millis(30));
assert_eq!(block_on(handle), 1234);
}
#[test]
#[should_panic(expected = "worker exploded")]
fn spawn_blocking_panic_resumes_on_joiner() {
let _: u32 = block_on(spawn_blocking(|| -> u32 {
assert_eq!(1, 2, "worker exploded");
0u32
}));
}
}