use core::sync::atomic::AtomicU8;
use std::io;
use std::sync::Arc;
use std::time::{Duration, Instant};
use kovan::{Atomic, RetiredNode, Shared as Ptr, alloc_node, drop_node, pin, retire_node};
use super::inbox::{Inbox, Pop};
use super::task::{self, Runnable, Schedule, ScheduleInfo, Task};
use crate::sync::internal::{AtomicUsize, Ordering};
#[derive(Clone)]
pub(crate) struct PoolConfig {
pub(crate) max_threads: usize,
pub(crate) hooks: super::tuning::ThreadHooks,
pub(crate) keep_alive: Duration,
pub(crate) stack_size: Option<usize>,
pub(crate) name: Arc<dyn Fn() -> String + Send + Sync>,
}
const FREE: u8 = 0;
const BUSY: u8 = 1;
const IDLE: u8 = 2;
const WOKEN: u8 = 3;
#[repr(C)]
struct Named {
retired: RetiredNode,
thread: std::thread::Thread,
}
struct Slot {
state: AtomicU8,
thread: Atomic<Named>,
}
pub(crate) struct Pool {
queue: Inbox,
slots: Box<[Slot]>,
hint: AtomicUsize,
threads: AtomicUsize,
idle: AtomicUsize,
jobs: AtomicUsize,
owner: std::sync::OnceLock<std::sync::Weak<super::runtime::Inner>>,
config: PoolConfig,
}
struct PoolSched(Arc<Pool>);
unsafe impl Schedule for PoolSched {
fn schedule(&self, runnable: Runnable, _info: ScheduleInfo) {
let pool = Arc::clone(&self.0);
pool.submit(runnable);
}
}
impl Pool {
pub(crate) fn new(config: PoolConfig) -> Arc<Self> {
let queue = Inbox::new();
unsafe { queue.release() };
let slots = (0..config.max_threads.max(1))
.map(|_| Slot {
state: AtomicU8::new(FREE),
thread: Atomic::null(),
})
.collect();
Arc::new(Self {
queue,
slots,
hint: AtomicUsize::new(0),
threads: AtomicUsize::new(0),
idle: AtomicUsize::new(0),
jobs: AtomicUsize::new(0),
owner: std::sync::OnceLock::new(),
config,
})
}
pub(crate) fn spawn<F, T>(self: &Arc<Self>, f: F) -> Task<T>
where
F: FnOnce() -> T + Send + 'static,
T: Send + 'static,
{
let (runnable, task) = task::spawn(async move { f() }, PoolSched(Arc::clone(self)));
runnable.schedule();
task
}
#[cfg(test)]
pub(crate) fn threads(&self) -> usize {
self.threads.load(Ordering::SeqCst)
}
pub(crate) fn set_owner(&self, owner: std::sync::Weak<super::runtime::Inner>) {
let _ = self.owner.set(owner);
}
pub(crate) fn jobs(&self) -> usize {
self.jobs.load(Ordering::SeqCst)
}
fn run(&self, runnable: Runnable) {
runnable.run();
if self.jobs.fetch_sub(1, Ordering::SeqCst) == 1
&& let Some(owner) = self.owner.get().and_then(std::sync::Weak::upgrade)
{
super::paused::try_advance(&owner, None);
}
}
fn submit(self: &Arc<Self>, runnable: Runnable) {
self.jobs.fetch_add(1, Ordering::SeqCst);
self.queue.push(runnable);
if self.idle.load(Ordering::SeqCst) > 0 && self.wake_one() {
return;
}
self.start_thread();
}
fn wake_one(&self) -> bool {
let n = self.slots.len();
let from = self.hint.fetch_add(1, Ordering::Relaxed);
for i in (0..n).map(|k| (from + k) % n) {
let slot = &self.slots[i];
if slot
.state
.compare_exchange(IDLE, WOKEN, Ordering::SeqCst, Ordering::SeqCst)
.is_ok()
{
self.idle.fetch_sub(1, Ordering::SeqCst);
let guard = pin();
if let Some(named) = unsafe { slot.thread.load(Ordering::Acquire, &guard).as_ref() }
{
named.thread.unpark();
}
return true;
}
}
false
}
fn take(&self) -> Option<Runnable> {
let mut spins = 0u32;
loop {
if self.queue.try_take() {
let popped = unsafe {
let popped = self.queue.pop();
self.queue.release();
popped
};
match popped {
Pop::Item(runnable) => return Some(runnable),
Pop::Empty => return None,
Pop::Busy => {}
}
} else if self.queue.is_empty() {
return None;
}
spins += 1;
if spins.is_multiple_of(64) {
std::thread::yield_now();
} else {
core::hint::spin_loop();
}
}
}
fn start_thread(self: &Arc<Self>) {
if !self.count_thread() {
return;
}
let pool = Arc::clone(self);
let mut builder = std::thread::Builder::new().name((self.config.name)());
if let Some(size) = self.config.stack_size {
builder = builder.stack_size(size);
}
if let Err(err) = builder.spawn(move || pool.work()) {
self.threads.fetch_sub(1, Ordering::SeqCst);
self.cannot_start(err);
}
}
fn count_thread(&self) -> bool {
let mut n = self.threads.load(Ordering::SeqCst);
loop {
if n >= self.config.max_threads {
return false;
}
match self
.threads
.compare_exchange_weak(n, n + 1, Ordering::SeqCst, Ordering::SeqCst)
{
Ok(_) => return true,
Err(actual) => n = actual,
}
}
}
fn cannot_start(&self, _err: io::Error) {
if self.threads.load(Ordering::SeqCst) == 0 {
while let Some(runnable) = self.take() {
self.run(runnable);
}
}
}
fn claim_slot(&self) -> usize {
let n = self.slots.len();
let mut spins = 0u32;
loop {
for i in 0..n {
let slot = &self.slots[i];
if slot
.state
.compare_exchange(FREE, BUSY, Ordering::SeqCst, Ordering::SeqCst)
.is_ok()
{
let named = alloc_node(Named {
retired: RetiredNode::new(),
thread: std::thread::current(),
});
let guard = pin();
let new = unsafe { Ptr::from_raw(named) };
let old = slot.thread.swap(new, Ordering::AcqRel, &guard);
if !old.is_null() {
unsafe { retire_node(old.as_raw()) };
}
return i;
}
}
spins += 1;
if spins.is_multiple_of(64) {
std::thread::yield_now();
} else {
core::hint::spin_loop();
}
}
}
fn work(self: Arc<Self>) {
let _alive = Alive::new();
let _stopping = self.config.hooks.started();
let mut me = self.claim_slot();
loop {
while let Some(runnable) = self.take() {
self.run(runnable);
}
let slot = &self.slots[me];
self.idle.fetch_add(1, Ordering::SeqCst);
slot.state.store(IDLE, Ordering::SeqCst);
if !self.queue.is_empty() {
if slot
.state
.compare_exchange(IDLE, BUSY, Ordering::SeqCst, Ordering::SeqCst)
.is_ok()
{
self.idle.fetch_sub(1, Ordering::SeqCst);
} else {
slot.state.store(BUSY, Ordering::SeqCst);
}
continue;
}
let deadline = Instant::now() + self.config.keep_alive;
loop {
if slot.state.load(Ordering::SeqCst) == WOKEN {
break;
}
let now = Instant::now();
if now >= deadline {
break;
}
std::thread::park_timeout(deadline - now);
}
if slot
.state
.compare_exchange(IDLE, FREE, Ordering::SeqCst, Ordering::SeqCst)
.is_err()
{
slot.state.store(BUSY, Ordering::SeqCst);
continue;
}
self.idle.fetch_sub(1, Ordering::SeqCst);
self.threads.fetch_sub(1, Ordering::SeqCst);
if !self.queue.is_empty() && self.count_thread() {
me = self.claim_slot();
continue;
}
return;
}
}
}
impl Drop for Pool {
fn drop(&mut self) {
let guard = pin();
for slot in self.slots.iter() {
let named = slot.thread.load(Ordering::Acquire, &guard);
if !named.is_null() {
unsafe { drop_node(named.as_raw()) };
}
}
}
}
static ALIVE: core::sync::atomic::AtomicUsize = core::sync::atomic::AtomicUsize::new(0);
pub(crate) async fn call<T, F>(call: F) -> io::Result<T>
where
T: Send + 'static,
F: FnOnce() -> io::Result<T> + Send + 'static,
{
match crate::runtime::spawn_blocking(call).join().await {
Ok(result) => result.inspect_err(crate::rt::counters::io_error),
Err(lost) => Err(lost.into()),
}
}
pub(crate) fn threads_alive() -> usize {
ALIVE.load(core::sync::atomic::Ordering::Relaxed)
}
struct Alive;
impl Alive {
fn new() -> Self {
ALIVE.fetch_add(1, core::sync::atomic::Ordering::Relaxed);
Self
}
}
impl Drop for Alive {
fn drop(&mut self) {
ALIVE.fetch_sub(1, core::sync::atomic::Ordering::Relaxed);
}
}
#[cfg(all(test, not(loom)))]
mod tests {
use std::sync::Arc;
use std::time::{Duration, Instant};
use super::{Pool, PoolConfig};
fn pool(max: usize, keep_alive: Duration) -> Arc<Pool> {
Pool::new(PoolConfig {
max_threads: max,
hooks: crate::rt::tuning::ThreadHooks::default(),
keep_alive,
stack_size: None,
name: Arc::new(|| "test-blocking".to_string()),
})
}
fn wait_for(what: &str, mut cond: impl FnMut() -> bool) {
let deadline = Instant::now() + Duration::from_secs(20);
let mut pause = Duration::from_micros(50);
while !cond() {
assert!(Instant::now() < deadline, "timed out waiting for {what}");
std::thread::sleep(pause);
pause = (pause * 2).min(Duration::from_millis(20));
}
}
#[test]
fn idle_threads_exit_after_the_keep_alive() {
let pool = pool(4, Duration::from_millis(20));
let tasks: Vec<_> = (0..4).map(|i| pool.spawn(move || i)).collect();
for task in tasks {
crate::sync::internal::block_on(task);
}
assert!(pool.threads() >= 1);
wait_for("the idle threads to exit", || pool.threads() == 0);
assert_eq!(crate::sync::internal::block_on(pool.spawn(|| 9)), 9);
}
#[test]
fn the_pool_never_runs_more_threads_than_its_maximum() {
let pool = pool(2, Duration::from_secs(5));
let peak = Arc::new(std::sync::atomic::AtomicUsize::new(0));
let live = Arc::new(std::sync::atomic::AtomicUsize::new(0));
let tasks: Vec<_> = (0..32)
.map(|_| {
let (peak, live) = (Arc::clone(&peak), Arc::clone(&live));
pool.spawn(move || {
use std::sync::atomic::Ordering::SeqCst;
let now = live.fetch_add(1, SeqCst) + 1;
peak.fetch_max(now, SeqCst);
std::thread::sleep(Duration::from_micros(300));
live.fetch_sub(1, SeqCst);
})
})
.collect();
for task in tasks {
crate::sync::internal::block_on(task);
}
assert!(peak.load(std::sync::atomic::Ordering::SeqCst) <= 2);
assert!(pool.threads() <= 2);
}
#[test]
fn a_job_submitted_as_the_thread_goes_idle_is_not_missed() {
use std::sync::atomic::Ordering::SeqCst;
let pool = pool(1, Duration::from_secs(30));
let done = Arc::new(std::sync::atomic::AtomicUsize::new(0));
for i in 0..20_000usize {
let ran = Arc::clone(&done);
pool.spawn(move || ran.store(i + 1, SeqCst)).detach();
let deadline = Instant::now() + Duration::from_secs(5);
while done.load(SeqCst) != i + 1 {
assert!(
Instant::now() < deadline,
"job {i} waited for the keep-alive"
);
core::hint::spin_loop();
}
}
assert_eq!(pool.threads(), 1);
}
#[test]
fn every_job_runs_once_while_threads_come_and_go() {
use std::sync::atomic::Ordering::SeqCst;
const SUBMITTERS: usize = 4;
const EACH: usize = 2_000;
let pool = pool(2, Duration::from_millis(1));
let ran = Arc::new(std::sync::atomic::AtomicUsize::new(0));
let submitters: Vec<_> = (0..SUBMITTERS)
.map(|s| {
let (pool, ran) = (Arc::clone(&pool), Arc::clone(&ran));
std::thread::spawn(move || {
for i in 0..EACH {
let ran = Arc::clone(&ran);
let task = pool.spawn(move || ran.fetch_add(1, SeqCst));
if (s + i) % 3 == 0 {
crate::sync::internal::block_on(task);
} else {
task.detach();
}
if i % 97 == 0 {
std::thread::sleep(Duration::from_millis(2));
}
}
})
})
.collect();
for s in submitters {
s.join().unwrap();
}
wait_for("every job to run", || ran.load(SeqCst) == SUBMITTERS * EACH);
assert_eq!(ran.load(SeqCst), SUBMITTERS * EACH);
assert!(pool.threads() <= 2);
wait_for("the jobs to be counted out", || pool.jobs() == 0);
}
}