mirror of
https://github.com/async-rs/async-std.git
synced 2025-03-28 20:16:41 +00:00
Spawn more than one blocking thread (#475)
* Spawn more than 1 blocking thread * Fix a bug * Fix check when the thread is last sleeping
This commit is contained in:
parent
84880c4d8b
commit
f588ba6bdd
2 changed files with 49 additions and 69 deletions
|
@ -1,4 +1,4 @@
|
||||||
use std::sync::atomic::{AtomicU64, Ordering};
|
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||||
use std::thread;
|
use std::thread;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
|
@ -8,8 +8,6 @@ use once_cell::sync::Lazy;
|
||||||
use crate::task::{JoinHandle, Task};
|
use crate::task::{JoinHandle, Task};
|
||||||
use crate::utils::{abort_on_panic, random};
|
use crate::utils::{abort_on_panic, random};
|
||||||
|
|
||||||
type Runnable = async_task::Task<Task>;
|
|
||||||
|
|
||||||
/// Spawns a blocking task.
|
/// Spawns a blocking task.
|
||||||
///
|
///
|
||||||
/// The task will be spawned onto a thread pool specifically dedicated to blocking tasks. This
|
/// The task will be spawned onto a thread pool specifically dedicated to blocking tasks. This
|
||||||
|
@ -44,14 +42,16 @@ where
|
||||||
F: FnOnce() -> T + Send + 'static,
|
F: FnOnce() -> T + Send + 'static,
|
||||||
T: Send + 'static,
|
T: Send + 'static,
|
||||||
{
|
{
|
||||||
|
let schedule = |task| POOL.sender.send(task).unwrap();
|
||||||
let (task, handle) = async_task::spawn(async { f() }, schedule, Task::new(None));
|
let (task, handle) = async_task::spawn(async { f() }, schedule, Task::new(None));
|
||||||
task.schedule();
|
task.schedule();
|
||||||
JoinHandle::new(handle)
|
JoinHandle::new(handle)
|
||||||
}
|
}
|
||||||
|
|
||||||
const MAX_THREADS: u64 = 10_000;
|
type Runnable = async_task::Task<Task>;
|
||||||
|
|
||||||
static DYNAMIC_THREAD_COUNT: AtomicU64 = AtomicU64::new(0);
|
/// The number of sleeping worker threads.
|
||||||
|
static SLEEPING: AtomicUsize = AtomicUsize::new(0);
|
||||||
|
|
||||||
struct Pool {
|
struct Pool {
|
||||||
sender: Sender<Runnable>,
|
sender: Sender<Runnable>,
|
||||||
|
@ -59,78 +59,52 @@ struct Pool {
|
||||||
}
|
}
|
||||||
|
|
||||||
static POOL: Lazy<Pool> = Lazy::new(|| {
|
static POOL: Lazy<Pool> = Lazy::new(|| {
|
||||||
for _ in 0..2 {
|
// Start a single worker thread waiting for the first task.
|
||||||
thread::Builder::new()
|
start_thread();
|
||||||
.name("async-std/blocking".to_string())
|
|
||||||
.spawn(|| {
|
|
||||||
abort_on_panic(|| {
|
|
||||||
for task in &POOL.receiver {
|
|
||||||
task.run();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.expect("cannot start a thread driving blocking tasks");
|
|
||||||
}
|
|
||||||
|
|
||||||
// We want to use an unbuffered channel here to help
|
|
||||||
// us drive our dynamic control. In effect, the
|
|
||||||
// kernel's scheduler becomes the queue, reducing
|
|
||||||
// the number of buffers that work must flow through
|
|
||||||
// before being acted on by a core. This helps keep
|
|
||||||
// latency snappy in the overall async system by
|
|
||||||
// reducing bufferbloat.
|
|
||||||
let (sender, receiver) = unbounded();
|
let (sender, receiver) = unbounded();
|
||||||
Pool { sender, receiver }
|
Pool { sender, receiver }
|
||||||
});
|
});
|
||||||
|
|
||||||
// Create up to MAX_THREADS dynamic blocking task worker threads.
|
fn start_thread() {
|
||||||
// Dynamic threads will terminate themselves if they don't
|
SLEEPING.fetch_add(1, Ordering::SeqCst);
|
||||||
// receive any work after between one and ten seconds.
|
|
||||||
fn maybe_create_another_blocking_thread() {
|
|
||||||
// We use a `Relaxed` atomic operation because
|
|
||||||
// it's just a heuristic, and would not lose correctness
|
|
||||||
// even if it's random.
|
|
||||||
let workers = DYNAMIC_THREAD_COUNT.load(Ordering::Relaxed);
|
|
||||||
if workers >= MAX_THREADS {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let n_to_spawn = std::cmp::min(2 + (workers / 10), 10);
|
// Generate a random duration of time between 1 second and 10 seconds. If the thread doesn't
|
||||||
|
// receive the next task in this duration of time, it will stop running.
|
||||||
|
let timeout = Duration::from_millis(1000 + u64::from(random(9_000)));
|
||||||
|
|
||||||
for _ in 0..n_to_spawn {
|
thread::Builder::new()
|
||||||
// We want to avoid having all threads terminate at
|
.name("async-std/blocking".to_string())
|
||||||
// exactly the same time, causing thundering herd
|
.spawn(move || {
|
||||||
// effects. We want to stagger their destruction over
|
loop {
|
||||||
// 10 seconds or so to make the costs fade into
|
let task = match POOL.receiver.recv_timeout(timeout) {
|
||||||
// background noise.
|
Ok(task) => task,
|
||||||
//
|
Err(_) => {
|
||||||
// Generate a simple random number of milliseconds
|
// Check whether this is the last sleeping thread.
|
||||||
let rand_sleep_ms = u64::from(random(10_000));
|
if SLEEPING.fetch_sub(1, Ordering::SeqCst) == 1 {
|
||||||
|
// If so, then restart the thread to make sure there is always at least
|
||||||
|
// one sleeping thread.
|
||||||
|
if SLEEPING.compare_and_swap(0, 1, Ordering::SeqCst) == 0 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
thread::Builder::new()
|
// Stop the thread.
|
||||||
.name("async-std/blocking".to_string())
|
return;
|
||||||
.spawn(move || {
|
}
|
||||||
let wait_limit = Duration::from_millis(1000 + rand_sleep_ms);
|
};
|
||||||
|
|
||||||
DYNAMIC_THREAD_COUNT.fetch_add(1, Ordering::Relaxed);
|
// If there are no sleeping threads, then start one to make sure there is always at
|
||||||
while let Ok(task) = POOL.receiver.recv_timeout(wait_limit) {
|
// least one sleeping thread.
|
||||||
abort_on_panic(|| task.run());
|
if SLEEPING.fetch_sub(1, Ordering::SeqCst) == 1 {
|
||||||
|
start_thread();
|
||||||
}
|
}
|
||||||
DYNAMIC_THREAD_COUNT.fetch_sub(1, Ordering::Relaxed);
|
|
||||||
})
|
|
||||||
.expect("cannot start a dynamic thread driving blocking tasks");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Enqueues work, attempting to send to the threadpool in a
|
// Run the task.
|
||||||
// nonblocking way and spinning up another worker thread if
|
abort_on_panic(|| task.run());
|
||||||
// there is not a thread ready to accept the work.
|
|
||||||
pub(crate) fn schedule(task: Runnable) {
|
SLEEPING.fetch_add(1, Ordering::SeqCst);
|
||||||
if let Err(err) = POOL.sender.try_send(task) {
|
}
|
||||||
// We were not able to send to the channel without
|
})
|
||||||
// blocking. Try to spin up another thread and then
|
.expect("cannot start a blocking thread");
|
||||||
// retry sending while blocking.
|
|
||||||
maybe_create_another_blocking_thread();
|
|
||||||
POOL.sender.send(err.into_inner()).unwrap();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,7 +25,13 @@ pub fn random(n: u32) -> u32 {
|
||||||
use std::num::Wrapping;
|
use std::num::Wrapping;
|
||||||
|
|
||||||
thread_local! {
|
thread_local! {
|
||||||
static RNG: Cell<Wrapping<u32>> = Cell::new(Wrapping(1_406_868_647));
|
static RNG: Cell<Wrapping<u32>> = {
|
||||||
|
// Take the address of a local value as seed.
|
||||||
|
let mut x = 0i32;
|
||||||
|
let r = &mut x;
|
||||||
|
let addr = r as *mut i32 as usize;
|
||||||
|
Cell::new(Wrapping(addr as u32))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
RNG.with(|rng| {
|
RNG.with(|rng| {
|
||||||
|
|
Loading…
Reference in a new issue