rt: use task::Inject with current_thread scheduler (#5702)

Previously, the current_thread scheduler used its own injection queue
instead of sharing the same one as the multi-threaded scheduler. This
patch updates the current_thread scheduler to use the same injection
queue as the multi-threaded one (`task::Inject`).

`task::Inject` includes an optimization where it does not need to
acquire the mutex if the queue is empty.
This commit is contained in:
Carl Lerche
2023-05-21 00:08:00 +00:00
committed by GitHub
parent ddd7250e62
commit 93bde0870f
8 changed files with 247 additions and 140 deletions
+5
View File
@@ -40,6 +40,11 @@ name = "sync_watch"
path = "sync_watch.rs" path = "sync_watch.rs"
harness = false harness = false
[[bench]]
name = "rt_current_thread"
path = "rt_current_thread.rs"
harness = false
[[bench]] [[bench]]
name = "rt_multi_threaded" name = "rt_multi_threaded"
path = "rt_multi_threaded.rs" path = "rt_multi_threaded.rs"
+83
View File
@@ -0,0 +1,83 @@
//! Benchmark implementation details of the threaded scheduler. These benches are
//! intended to be used as a form of regression testing and not as a general
//! purpose benchmark demonstrating real-world performance.
use tokio::runtime::{self, Runtime};
use bencher::{benchmark_group, benchmark_main, Bencher};
const NUM_SPAWN: usize = 1_000;
fn spawn_many_local(b: &mut Bencher) {
let rt = rt();
let mut handles = Vec::with_capacity(NUM_SPAWN);
b.iter(|| {
rt.block_on(async {
for _ in 0..NUM_SPAWN {
handles.push(tokio::spawn(async move {}));
}
for handle in handles.drain(..) {
handle.await.unwrap();
}
});
});
}
fn spawn_many_remote_idle(b: &mut Bencher) {
let rt = rt();
let rt_handle = rt.handle();
let mut handles = Vec::with_capacity(NUM_SPAWN);
b.iter(|| {
for _ in 0..NUM_SPAWN {
handles.push(rt_handle.spawn(async {}));
}
rt.block_on(async {
for handle in handles.drain(..) {
handle.await.unwrap();
}
});
});
}
fn spawn_many_remote_busy(b: &mut Bencher) {
let rt = rt();
let rt_handle = rt.handle();
let mut handles = Vec::with_capacity(NUM_SPAWN);
rt.spawn(async {
fn iter() {
tokio::spawn(async { iter() });
}
iter()
});
b.iter(|| {
for _ in 0..NUM_SPAWN {
handles.push(rt_handle.spawn(async {}));
}
rt.block_on(async {
for handle in handles.drain(..) {
handle.await.unwrap();
}
});
});
}
fn rt() -> Runtime {
runtime::Builder::new_current_thread().build().unwrap()
}
benchmark_group!(
scheduler,
spawn_many_local,
spawn_many_remote_idle,
spawn_many_remote_busy
);
benchmark_main!(scheduler);
+24 -42
View File
@@ -1,8 +1,8 @@
use crate::future::poll_fn; use crate::future::poll_fn;
use crate::loom::sync::atomic::AtomicBool; use crate::loom::sync::atomic::AtomicBool;
use crate::loom::sync::{Arc, Mutex}; use crate::loom::sync::Arc;
use crate::runtime::driver::{self, Driver}; use crate::runtime::driver::{self, Driver};
use crate::runtime::task::{self, JoinHandle, OwnedTasks, Schedule, Task}; use crate::runtime::task::{self, Inject, JoinHandle, OwnedTasks, Schedule, Task};
use crate::runtime::{blocking, context, scheduler, Config}; use crate::runtime::{blocking, context, scheduler, Config};
use crate::runtime::{MetricsBatch, SchedulerMetrics, WorkerMetrics}; use crate::runtime::{MetricsBatch, SchedulerMetrics, WorkerMetrics};
use crate::sync::notify::Notify; use crate::sync::notify::Notify;
@@ -66,8 +66,8 @@ struct Core {
/// Scheduler state shared between threads. /// Scheduler state shared between threads.
struct Shared { struct Shared {
/// Remote run queue. None if the `Runtime` has been dropped. /// Remote run queue
queue: Mutex<Option<VecDeque<Notified>>>, inject: Inject<Arc<Handle>>,
/// Collection of all active tasks spawned onto this executor. /// Collection of all active tasks spawned onto this executor.
owned: OwnedTasks<Arc<Handle>>, owned: OwnedTasks<Arc<Handle>>,
@@ -115,7 +115,7 @@ impl CurrentThread {
let handle = Arc::new(Handle { let handle = Arc::new(Handle {
shared: Shared { shared: Shared {
queue: Mutex::new(Some(VecDeque::with_capacity(INITIAL_CAPACITY))), inject: Inject::new(),
owned: OwnedTasks::new(), owned: OwnedTasks::new(),
woken: AtomicBool::new(false), woken: AtomicBool::new(false),
config, config,
@@ -217,15 +217,12 @@ impl CurrentThread {
drop(task); drop(task);
} }
// Drain remote queue and set it to None // Close the injection queue
let remote_queue = handle.shared.queue.lock().take(); handle.shared.inject.close();
// Using `Option::take` to replace the shared queue with `None`. // Drain remote queue
// We already shut down every task, so we just need to drop the task. while let Some(task) = handle.shared.inject.pop() {
if let Some(remote_queue) = remote_queue { drop(task);
for task in remote_queue {
drop(task);
}
} }
assert!(handle.shared.owned.is_empty()); assert!(handle.shared.owned.is_empty());
@@ -259,9 +256,12 @@ impl Core {
fn next_task(&mut self, handle: &Handle) -> Option<Notified> { fn next_task(&mut self, handle: &Handle) -> Option<Notified> {
if self.tick % handle.shared.config.global_queue_interval == 0 { if self.tick % handle.shared.config.global_queue_interval == 0 {
handle.pop().or_else(|| self.next_local_task(handle)) handle
.next_remote_task()
.or_else(|| self.next_local_task(handle))
} else { } else {
self.next_local_task(handle).or_else(|| handle.pop()) self.next_local_task(handle)
.or_else(|| handle.next_remote_task())
} }
} }
@@ -440,14 +440,11 @@ impl Handle {
}; };
let local = &mut core.tasks; let local = &mut core.tasks;
let mut injection = self.shared.queue.lock(); if self.shared.inject.is_closed() {
let injection = if let Some(injection) = injection.as_mut() {
injection
} else {
return; return;
}; }
traces = trace_current_thread(&self.shared.owned, local, injection) traces = trace_current_thread(&self.shared.owned, local, &self.shared.inject)
.into_iter() .into_iter()
.map(dump::Task::new) .map(dump::Task::new)
.collect(); .collect();
@@ -461,11 +458,8 @@ impl Handle {
dump::Dump::new(traces) dump::Dump::new(traces)
} }
fn pop(&self) -> Option<Notified> { fn next_remote_task(&self) -> Option<Notified> {
match self.shared.queue.lock().as_mut() { self.shared.inject.pop()
Some(queue) => queue.pop_front(),
None => None,
}
} }
fn waker_ref(me: &Arc<Self>) -> WakerRef<'_> { fn waker_ref(me: &Arc<Self>) -> WakerRef<'_> {
@@ -488,14 +482,7 @@ cfg_metrics! {
} }
pub(crate) fn injection_queue_depth(&self) -> usize { pub(crate) fn injection_queue_depth(&self) -> usize {
// TODO: avoid having to lock. The multi-threaded injection queue self.shared.inject.len()
// could probably be used here.
self.shared
.queue
.lock()
.as_ref()
.map(|queue| queue.len())
.unwrap_or(0)
} }
pub(crate) fn worker_metrics(&self, worker: usize) -> &WorkerMetrics { pub(crate) fn worker_metrics(&self, worker: usize) -> &WorkerMetrics {
@@ -549,14 +536,9 @@ impl Schedule for Arc<Handle> {
// Track that a task was scheduled from **outside** of the runtime. // Track that a task was scheduled from **outside** of the runtime.
self.shared.scheduler_metrics.inc_remote_schedule_count(); self.shared.scheduler_metrics.inc_remote_schedule_count();
// If the queue is None, then the runtime has shut down. We // Schedule the task
// don't need to do anything with the notification in that case. self.shared.inject.push(task);
let mut guard = self.shared.queue.lock(); self.driver.unpark();
if let Some(queue) = guard.as_mut() {
queue.push_back(task);
drop(guard);
self.driver.unpark();
}
} }
}); });
} }
+4 -8
View File
@@ -278,15 +278,11 @@ impl<T: Future, S: Schedule> Core<T, S> {
} }
} }
cfg_rt_multi_thread! {
impl Header {
pub(super) unsafe fn set_next(&self, next: Option<NonNull<Header>>) {
self.queue_next.with_mut(|ptr| *ptr = next);
}
}
}
impl Header { impl Header {
pub(super) unsafe fn set_next(&self, next: Option<NonNull<Header>>) {
self.queue_next.with_mut(|ptr| *ptr = next);
}
// safety: The caller must guarantee exclusive access to this field, and // safety: The caller must guarantee exclusive access to this field, and
// must ensure that the id is either 0 or the id of the OwnedTasks // must ensure that the id is either 0 or the id of the OwnedTasks
// containing this task. // containing this task.
+75 -69
View File
@@ -52,6 +52,12 @@ impl<T: 'static> Inject<T> {
self.len() == 0 self.len() == 0
} }
// Kind of annoying to have to include the cfg here
#[cfg(any(tokio_taskdump, all(feature = "rt-multi-thread", not(tokio_wasi))))]
pub(crate) fn is_closed(&self) -> bool {
self.pointers.lock().is_closed
}
/// Closes the injection queue, returns `true` if the queue is open when the /// Closes the injection queue, returns `true` if the queue is open when the
/// transition is made. /// transition is made.
pub(crate) fn close(&self) -> bool { pub(crate) fn close(&self) -> bool {
@@ -65,10 +71,6 @@ impl<T: 'static> Inject<T> {
true true
} }
pub(crate) fn is_closed(&self) -> bool {
self.pointers.lock().is_closed
}
pub(crate) fn len(&self) -> usize { pub(crate) fn len(&self) -> usize {
self.len.load(Acquire) self.len.load(Acquire)
} }
@@ -104,71 +106,6 @@ impl<T: 'static> Inject<T> {
self.len.store(len + 1, Release); self.len.store(len + 1, Release);
} }
/// Pushes several values into the queue.
#[inline]
pub(crate) fn push_batch<I>(&self, mut iter: I)
where
I: Iterator<Item = task::Notified<T>>,
{
let first = match iter.next() {
Some(first) => first.into_raw(),
None => return,
};
// Link up all the tasks.
let mut prev = first;
let mut counter = 1;
// We are going to be called with an `std::iter::Chain`, and that
// iterator overrides `for_each` to something that is easier for the
// compiler to optimize than a loop.
iter.for_each(|next| {
let next = next.into_raw();
// safety: Holding the Notified for a task guarantees exclusive
// access to the `queue_next` field.
set_next(prev, Some(next));
prev = next;
counter += 1;
});
// Now that the tasks are linked together, insert them into the
// linked list.
self.push_batch_inner(first, prev, counter);
}
/// Inserts several tasks that have been linked together into the queue.
///
/// The provided head and tail may be be the same task. In this case, a
/// single task is inserted.
#[inline]
fn push_batch_inner(
&self,
batch_head: NonNull<task::Header>,
batch_tail: NonNull<task::Header>,
num: usize,
) {
debug_assert!(get_next(batch_tail).is_none());
let mut p = self.pointers.lock();
if let Some(tail) = p.tail {
set_next(tail, Some(batch_head));
} else {
p.head = Some(batch_head);
}
p.tail = Some(batch_tail);
// Increment the count.
//
// safety: All updates to the len atomic are guarded by the mutex. As
// such, a non-atomic load followed by a store is safe.
let len = unsafe { self.len.unsync_load() };
self.len.store(len + num, Release);
}
pub(crate) fn pop(&self) -> Option<task::Notified<T>> { pub(crate) fn pop(&self) -> Option<task::Notified<T>> {
// Fast path, if len == 0, then there are no values // Fast path, if len == 0, then there are no values
if self.is_empty() { if self.is_empty() {
@@ -201,6 +138,75 @@ impl<T: 'static> Inject<T> {
} }
} }
cfg_rt_multi_thread! {
impl<T: 'static> Inject<T> {
/// Pushes several values into the queue.
#[inline]
pub(crate) fn push_batch<I>(&self, mut iter: I)
where
I: Iterator<Item = task::Notified<T>>,
{
let first = match iter.next() {
Some(first) => first.into_raw(),
None => return,
};
// Link up all the tasks.
let mut prev = first;
let mut counter = 1;
// We are going to be called with an `std::iter::Chain`, and that
// iterator overrides `for_each` to something that is easier for the
// compiler to optimize than a loop.
iter.for_each(|next| {
let next = next.into_raw();
// safety: Holding the Notified for a task guarantees exclusive
// access to the `queue_next` field.
set_next(prev, Some(next));
prev = next;
counter += 1;
});
// Now that the tasks are linked together, insert them into the
// linked list.
self.push_batch_inner(first, prev, counter);
}
/// Inserts several tasks that have been linked together into the queue.
///
/// The provided head and tail may be be the same task. In this case, a
/// single task is inserted.
#[inline]
fn push_batch_inner(
&self,
batch_head: NonNull<task::Header>,
batch_tail: NonNull<task::Header>,
num: usize,
) {
debug_assert!(get_next(batch_tail).is_none());
let mut p = self.pointers.lock();
if let Some(tail) = p.tail {
set_next(tail, Some(batch_head));
} else {
p.head = Some(batch_head);
}
p.tail = Some(batch_tail);
// Increment the count.
//
// safety: All updates to the len atomic are guarded by the mutex. As
// such, a non-atomic load followed by a store is safe.
let len = unsafe { self.len.unsync_load() };
self.len.store(len + num, Release);
}
}
}
impl<T: 'static> Drop for Inject<T> { impl<T: 'static> Drop for Inject<T> {
fn drop(&mut self) { fn drop(&mut self) {
if !std::thread::panicking() { if !std::thread::panicking() {
+15 -19
View File
@@ -182,10 +182,8 @@ mod id;
#[cfg_attr(not(tokio_unstable), allow(unreachable_pub))] #[cfg_attr(not(tokio_unstable), allow(unreachable_pub))]
pub use id::{id, try_id, Id}; pub use id::{id, try_id, Id};
cfg_rt_multi_thread! { mod inject;
mod inject; pub(super) use self::inject::Inject;
pub(super) use self::inject::Inject;
}
#[cfg(feature = "rt")] #[cfg(feature = "rt")]
mod abort; mod abort;
@@ -370,25 +368,23 @@ impl<S: 'static> Notified<S> {
} }
} }
cfg_rt_multi_thread! { impl<S: 'static> Notified<S> {
impl<S: 'static> Notified<S> { unsafe fn from_raw(ptr: NonNull<Header>) -> Notified<S> {
unsafe fn from_raw(ptr: NonNull<Header>) -> Notified<S> { Notified(Task::from_raw(ptr))
Notified(Task::from_raw(ptr))
}
} }
}
impl<S: 'static> Task<S> { impl<S: 'static> Task<S> {
fn into_raw(self) -> NonNull<Header> { fn into_raw(self) -> NonNull<Header> {
let ret = self.raw.header_ptr(); let ret = self.raw.header_ptr();
mem::forget(self); mem::forget(self);
ret ret
}
} }
}
impl<S: 'static> Notified<S> { impl<S: 'static> Notified<S> {
fn into_raw(self) -> NonNull<Header> { fn into_raw(self) -> NonNull<Header> {
self.0.into_raw() self.0.into_raw()
}
} }
} }
+6 -2
View File
@@ -1,5 +1,6 @@
use crate::loom::sync::Arc; use crate::loom::sync::Arc;
use crate::runtime::scheduler::current_thread; use crate::runtime::scheduler::current_thread;
use crate::runtime::task::Inject;
use backtrace::BacktraceFrame; use backtrace::BacktraceFrame;
use std::cell::Cell; use std::cell::Cell;
use std::collections::VecDeque; use std::collections::VecDeque;
@@ -236,11 +237,14 @@ impl<T: Future> Future for Root<T> {
pub(in crate::runtime) fn trace_current_thread( pub(in crate::runtime) fn trace_current_thread(
owned: &OwnedTasks<Arc<current_thread::Handle>>, owned: &OwnedTasks<Arc<current_thread::Handle>>,
local: &mut VecDeque<Notified<Arc<current_thread::Handle>>>, local: &mut VecDeque<Notified<Arc<current_thread::Handle>>>,
injection: &mut VecDeque<Notified<Arc<current_thread::Handle>>>, injection: &Inject<Arc<current_thread::Handle>>,
) -> Vec<Trace> { ) -> Vec<Trace> {
// clear the local and injection queues // clear the local and injection queues
local.clear(); local.clear();
injection.clear();
while let Some(task) = injection.pop() {
drop(task);
}
// notify each task // notify each task
let mut tasks = vec![]; let mut tasks = vec![];
+35
View File
@@ -1317,4 +1317,39 @@ rt_test! {
} }
}); });
} }
#[test]
#[cfg(not(target_os="wasi"))]
fn shutdown_concurrent_spawn() {
const NUM_TASKS: usize = 10_000;
for _ in 0..5 {
let (tx, rx) = std::sync::mpsc::channel();
let rt = rt();
let mut txs = vec![];
for _ in 0..NUM_TASKS {
let (tx, rx) = tokio::sync::oneshot::channel();
txs.push(tx);
rt.spawn(async move {
rx.await.unwrap();
});
}
// Prime the tasks
rt.block_on(async { tokio::task::yield_now().await });
let th = std::thread::spawn(move || {
tx.send(()).unwrap();
for tx in txs.drain(..) {
let _ = tx.send(());
}
});
rx.recv().unwrap();
drop(rt);
th.join().unwrap();
}
}
} }