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::{AtomicU64, 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>,
pub(crate) cpu_mask: Option<super::runtime::CpuMask>,
}
const FREE: u8 = 0;
const BUSY: u8 = 1;
const IDLE: u8 = 2;
const WOKEN: u8 = 3;
const WORD: usize = u64::BITS as usize;
#[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]>,
named: AtomicUsize,
index: Box<[AtomicU64]>,
hint: AtomicUsize,
threads: AtomicUsize,
idle: AtomicUsize,
jobs: Arc<AtomicUsize>,
queued: 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 n = config.max_threads.max(1);
let slots = (0..n)
.map(|_| Slot {
state: AtomicU8::new(FREE),
thread: Atomic::null(),
})
.collect();
let index = (0..n.div_ceil(WORD)).map(|_| AtomicU64::new(0)).collect();
let pool = Arc::new(Self {
queue,
slots,
named: AtomicUsize::new(0),
index,
hint: AtomicUsize::new(0),
threads: AtomicUsize::new(0),
idle: AtomicUsize::new(0),
jobs: Arc::new(AtomicUsize::new(0)),
queued: AtomicUsize::new(0),
owner: std::sync::OnceLock::new(),
config,
});
take_out_gone();
pools().insert(Arc::as_ptr(&pool) as usize, Arc::downgrade(&pool));
pool
}
pub(crate) fn queued(&self) -> usize {
self.queued.load(Ordering::Relaxed)
}
pub(crate) fn threads_now(&self) -> usize {
self.threads.load(Ordering::Relaxed)
}
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_counter(&self) -> Arc<AtomicUsize> {
Arc::clone(&self.jobs)
}
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.queued.fetch_add(1, Ordering::Relaxed);
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 words = self.index.len();
let from = self.hint.fetch_add(1, Ordering::Relaxed) % n;
let (first, low) = (from / WORD, from % WORD);
for step in 0..=words {
let w = (first + step) % words;
let mut bits = self.index[w].load(Ordering::SeqCst);
if step == 0 {
bits &= !0u64 << low;
} else if step == words {
bits &= !(!0u64 << low);
}
while bits != 0 {
let i = w * WORD + bits.trailing_zeros() as usize;
bits &= bits - 1;
if self.claim(i) {
return true;
}
}
}
false
}
fn claim(&self, i: usize) -> bool {
let slot = &self.slots[i];
if slot
.state
.compare_exchange(IDLE, WOKEN, Ordering::SeqCst, Ordering::SeqCst)
.is_err()
{
return false;
}
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();
}
true
}
fn set_bit(&self, i: usize) {
self.index[i / WORD].fetch_or(1 << (i % WORD), Ordering::SeqCst);
}
fn clear_bit(&self, i: usize) {
self.index[i / WORD].fetch_and(!(1 << (i % WORD)), Ordering::SeqCst);
}
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) => {
self.queued.fetch_sub(1, Ordering::Relaxed);
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()) };
}
self.named.fetch_max(i + 1, Ordering::Relaxed);
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();
if let Some(mask) = &self.config.cpu_mask {
super::runtime::apply_mask(mask);
}
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);
self.set_bit(me);
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);
}
self.clear_bit(me);
continue;
}
let deadline = Instant::now() + self.config.keep_alive;
let mut left = self.config.keep_alive;
while slot.state.load(Ordering::SeqCst) != WOKEN && !left.is_zero() {
std::thread::park_timeout(left);
if slot.state.load(Ordering::SeqCst) == WOKEN {
break;
}
left = deadline.saturating_duration_since(Instant::now());
}
self.clear_bit(me);
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) {
POOLS_GONE.fetch_add(1, core::sync::atomic::Ordering::Release);
let named = self.named.load(Ordering::Relaxed);
if named == 0 {
return;
}
let guard = pin();
for slot in &self.slots[..named] {
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);
fn pools() -> &'static kovan_map::HashMap<usize, std::sync::Weak<Pool>> {
static POOLS: std::sync::OnceLock<kovan_map::HashMap<usize, std::sync::Weak<Pool>>> =
std::sync::OnceLock::new();
POOLS.get_or_init(|| kovan_map::HashMap::with_capacity(64))
}
static POOLS_GONE: core::sync::atomic::AtomicUsize = core::sync::atomic::AtomicUsize::new(0);
fn take_out_gone() {
if POOLS_GONE.load(core::sync::atomic::Ordering::Relaxed) == 0
|| POOLS_GONE.swap(0, core::sync::atomic::Ordering::Acquire) == 0
{
return;
}
let gone: Vec<usize> = pools()
.iter()
.filter(|(_, pool)| pool.strong_count() == 0)
.map(|(key, _)| key)
.collect();
for key in gone {
drop(pools().remove(&key));
}
}
pub(crate) fn queued_everywhere() -> usize {
take_out_gone();
pools()
.values()
.filter_map(|pool| pool.upgrade())
.map(|pool| pool.queued())
.sum()
}
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()),
cpu_mask: None,
})
}
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);
}
fn bits(pool: &Pool) -> u32 {
use std::sync::atomic::Ordering::SeqCst;
pool.index.iter().map(|w| w.load(SeqCst).count_ones()).sum()
}
#[test]
fn every_job_runs_once_at_the_cap() {
use std::sync::atomic::Ordering::SeqCst;
const SUBMITTERS: usize = 8;
const EACH: usize = 500;
for max in [8, 70] {
let pool = pool(max, Duration::from_secs(5));
let ran = Arc::new(std::sync::atomic::AtomicUsize::new(0));
let submitters: Vec<_> = (0..SUBMITTERS)
.map(|_| {
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 || {
if i % 8 == 0 {
std::thread::sleep(Duration::from_micros(200));
}
ran.fetch_add(1, SeqCst)
});
if i % 2 == 0 {
crate::sync::internal::block_on(task);
} else {
task.detach();
}
}
})
})
.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() <= max);
wait_for("the jobs to be counted out", || pool.jobs() == 0);
}
}
#[test]
fn a_thread_going_idle_while_submitters_race_loses_no_job() {
use std::sync::atomic::Ordering::SeqCst;
let pool = pool(2, Duration::from_secs(30));
let done = Arc::new(std::sync::atomic::AtomicUsize::new(0));
let submitters: Vec<_> = (0..2)
.map(|_| {
let (pool, done) = (Arc::clone(&pool), Arc::clone(&done));
std::thread::spawn(move || {
for _ in 0..5_000 {
let before = done.load(SeqCst);
let ran = Arc::clone(&done);
pool.spawn(move || ran.fetch_add(1, SeqCst)).detach();
let deadline = Instant::now() + Duration::from_secs(5);
while done.load(SeqCst) == before {
assert!(Instant::now() < deadline, "a job waited for the keep-alive");
core::hint::spin_loop();
}
}
})
})
.collect();
for s in submitters {
s.join().unwrap();
}
wait_for("every job to run", || done.load(SeqCst) == 10_000);
wait_for("the jobs to be counted out", || pool.jobs() == 0);
}
#[test]
fn the_keep_alive_exit_leaves_no_bit_set() {
let pool = pool(70, Duration::from_millis(10));
for _ in 0..3 {
let tasks: Vec<_> = (0..70)
.map(|i| {
pool.spawn(move || std::thread::sleep(Duration::from_micros(100 * (i % 5))))
})
.collect();
for task in tasks {
crate::sync::internal::block_on(task);
}
wait_for("the idle threads to exit", || pool.threads() == 0);
assert_eq!(bits(&pool), 0);
assert_eq!(pool.idle.load(std::sync::atomic::Ordering::SeqCst), 0);
}
}
}