Skip to content

Commit 3a4aef1

Browse files
authored
runtime: reduce the lock contention in task spawn (#6001)
1 parent a0a58d7 commit 3a4aef1

File tree

13 files changed

+272
-142
lines changed

13 files changed

+272
-142
lines changed

benches/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ tokio = { version = "1.5.0", path = "../tokio", features = ["full"] }
1212
criterion = "0.5.1"
1313
rand = "0.8"
1414
rand_chacha = "0.3"
15+
num_cpus = "1.16.0"
1516

1617
[dev-dependencies]
1718
tokio-util = { version = "0.7.0", path = "../tokio-util", features = ["full"] }

tokio/src/runtime/builder.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1279,7 +1279,6 @@ cfg_rt_multi_thread! {
12791279
use crate::runtime::scheduler::MultiThreadAlt;
12801280

12811281
let core_threads = self.worker_threads.unwrap_or_else(num_cpus);
1282-
12831282
let (driver, driver_handle) = driver::Driver::new(self.get_cfg())?;
12841283

12851284
// Create the blocking pool

tokio/src/runtime/id.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use std::fmt;
2-
use std::num::NonZeroU64;
2+
use std::num::{NonZeroU32, NonZeroU64};
33

44
/// An opaque ID that uniquely identifies a runtime relative to all other currently
55
/// running runtimes.
@@ -39,6 +39,12 @@ impl From<NonZeroU64> for Id {
3939
}
4040
}
4141

42+
impl From<NonZeroU32> for Id {
43+
fn from(value: NonZeroU32) -> Self {
44+
Id(value.into())
45+
}
46+
}
47+
4248
impl fmt::Display for Id {
4349
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4450
self.0.fmt(f)

tokio/src/runtime/scheduler/current_thread/mod.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ impl CurrentThread {
132132
let handle = Arc::new(Handle {
133133
shared: Shared {
134134
inject: Inject::new(),
135-
owned: OwnedTasks::new(),
135+
owned: OwnedTasks::new(1),
136136
woken: AtomicBool::new(false),
137137
config,
138138
scheduler_metrics: SchedulerMetrics::new(),
@@ -248,7 +248,7 @@ fn shutdown2(mut core: Box<Core>, handle: &Handle) -> Box<Core> {
248248
// Drain the OwnedTasks collection. This call also closes the
249249
// collection, ensuring that no tasks are ever pushed after this
250250
// call returns.
251-
handle.shared.owned.close_and_shutdown_all();
251+
handle.shared.owned.close_and_shutdown_all(0);
252252

253253
// Drain local queue
254254
// We already shut down every task, so we just need to drop the task.
@@ -614,7 +614,7 @@ impl Schedule for Arc<Handle> {
614614
// If `None`, the runtime is shutting down, so there is no need to signal shutdown
615615
if let Some(core) = core.as_mut() {
616616
core.unhandled_panic = true;
617-
self.shared.owned.close_and_shutdown_all();
617+
self.shared.owned.close_and_shutdown_all(0);
618618
}
619619
}
620620
_ => unreachable!("runtime core not set in CURRENT thread-local"),

tokio/src/runtime/scheduler/multi_thread/worker.rs

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -287,7 +287,7 @@ pub(super) fn create(
287287
remotes: remotes.into_boxed_slice(),
288288
inject,
289289
idle,
290-
owned: OwnedTasks::new(),
290+
owned: OwnedTasks::new(size),
291291
synced: Mutex::new(Synced {
292292
idle: idle_synced,
293293
inject: inject_synced,
@@ -548,7 +548,6 @@ impl Context {
548548
}
549549

550550
core.pre_shutdown(&self.worker);
551-
552551
// Signal shutdown
553552
self.worker.handle.shutdown_core(core);
554553
Err(())
@@ -955,8 +954,16 @@ impl Core {
955954
/// Signals all tasks to shut down, and waits for them to complete. Must run
956955
/// before we enter the single-threaded phase of shutdown processing.
957956
fn pre_shutdown(&mut self, worker: &Worker) {
957+
// Start from a random inner list
958+
let start = self
959+
.rand
960+
.fastrand_n(worker.handle.shared.owned.get_shard_size() as u32);
958961
// Signal to all tasks to shut down.
959-
worker.handle.shared.owned.close_and_shutdown_all();
962+
worker
963+
.handle
964+
.shared
965+
.owned
966+
.close_and_shutdown_all(start as usize);
960967

961968
self.stats
962969
.submit(&worker.handle.shared.worker_metrics[worker.index]);

tokio/src/runtime/scheduler/multi_thread_alt/worker.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -307,7 +307,7 @@ pub(super) fn create(
307307
remotes: remotes.into_boxed_slice(),
308308
inject,
309309
idle,
310-
owned: OwnedTasks::new(),
310+
owned: OwnedTasks::new(num_cores),
311311
synced: Mutex::new(Synced {
312312
assigned_cores: (0..num_workers).map(|_| None).collect(),
313313
shutdown_cores: Vec::with_capacity(num_cores),
@@ -1460,7 +1460,9 @@ impl Shared {
14601460
}
14611461

14621462
pub(super) fn shutdown_core(&self, handle: &Handle, mut core: Box<Core>) {
1463-
self.owned.close_and_shutdown_all();
1463+
// Start from a random inner list
1464+
let start = core.rand.fastrand_n(self.owned.get_shard_size() as u32);
1465+
self.owned.close_and_shutdown_all(start as usize);
14641466

14651467
core.stats.submit(&self.worker_metrics[core.index]);
14661468

tokio/src/runtime/task/id.rs

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ use std::fmt;
2424
#[cfg_attr(docsrs, doc(cfg(all(feature = "rt", tokio_unstable))))]
2525
#[cfg_attr(not(tokio_unstable), allow(unreachable_pub))]
2626
#[derive(Clone, Copy, Debug, Hash, Eq, PartialEq)]
27-
pub struct Id(u64);
27+
pub struct Id(pub(crate) u64);
2828

2929
/// Returns the [`Id`] of the currently running task.
3030
///
@@ -74,11 +74,22 @@ impl fmt::Display for Id {
7474

7575
impl Id {
7676
pub(crate) fn next() -> Self {
77-
use crate::loom::sync::atomic::{Ordering::Relaxed, StaticAtomicU64};
77+
use crate::loom::sync::atomic::Ordering::Relaxed;
78+
use crate::loom::sync::atomic::StaticAtomicU64;
7879

79-
static NEXT_ID: StaticAtomicU64 = StaticAtomicU64::new(1);
80+
#[cfg(all(test, loom))]
81+
{
82+
crate::loom::lazy_static! {
83+
static ref NEXT_ID: StaticAtomicU64 = StaticAtomicU64::new(1);
84+
}
85+
Self(NEXT_ID.fetch_add(1, Relaxed))
86+
}
8087

81-
Self(NEXT_ID.fetch_add(1, Relaxed))
88+
#[cfg(not(all(test, loom)))]
89+
{
90+
static NEXT_ID: StaticAtomicU64 = StaticAtomicU64::new(1);
91+
Self(NEXT_ID.fetch_add(1, Relaxed))
92+
}
8293
}
8394

8495
pub(crate) fn as_u64(&self) -> u64 {

tokio/src/runtime/task/list.rs

Lines changed: 63 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,11 @@
88
99
use crate::future::Future;
1010
use crate::loom::cell::UnsafeCell;
11-
use crate::loom::sync::Mutex;
1211
use crate::runtime::task::{JoinHandle, LocalNotified, Notified, Schedule, Task};
13-
use crate::util::linked_list::{CountedLinkedList, Link, LinkedList};
12+
use crate::util::linked_list::{Link, LinkedList};
13+
use crate::util::sharded_list;
1414

15+
use crate::loom::sync::atomic::{AtomicBool, Ordering};
1516
use std::marker::PhantomData;
1617
use std::num::NonZeroU64;
1718

@@ -25,7 +26,7 @@ use std::num::NonZeroU64;
2526
// mixed up runtimes happen to have the same id.
2627

2728
cfg_has_atomic_u64! {
28-
use std::sync::atomic::{AtomicU64, Ordering};
29+
use std::sync::atomic::AtomicU64;
2930

3031
static NEXT_OWNED_TASKS_ID: AtomicU64 = AtomicU64::new(1);
3132

@@ -40,7 +41,7 @@ cfg_has_atomic_u64! {
4041
}
4142

4243
cfg_not_has_atomic_u64! {
43-
use std::sync::atomic::{AtomicU32, Ordering};
44+
use std::sync::atomic::AtomicU32;
4445

4546
static NEXT_OWNED_TASKS_ID: AtomicU32 = AtomicU32::new(1);
4647

@@ -55,30 +56,30 @@ cfg_not_has_atomic_u64! {
5556
}
5657

5758
pub(crate) struct OwnedTasks<S: 'static> {
58-
inner: Mutex<CountedOwnedTasksInner<S>>,
59+
list: List<S>,
5960
pub(crate) id: NonZeroU64,
61+
closed: AtomicBool,
6062
}
61-
struct CountedOwnedTasksInner<S: 'static> {
62-
list: CountedLinkedList<Task<S>, <Task<S> as Link>::Target>,
63-
closed: bool,
64-
}
63+
64+
type List<S> = sharded_list::ShardedList<Task<S>, <Task<S> as Link>::Target>;
65+
6566
pub(crate) struct LocalOwnedTasks<S: 'static> {
6667
inner: UnsafeCell<OwnedTasksInner<S>>,
6768
pub(crate) id: NonZeroU64,
6869
_not_send_or_sync: PhantomData<*const ()>,
6970
}
71+
7072
struct OwnedTasksInner<S: 'static> {
7173
list: LinkedList<Task<S>, <Task<S> as Link>::Target>,
7274
closed: bool,
7375
}
7476

7577
impl<S: 'static> OwnedTasks<S> {
76-
pub(crate) fn new() -> Self {
78+
pub(crate) fn new(num_cores: usize) -> Self {
79+
let shard_size = Self::gen_shared_list_size(num_cores);
7780
Self {
78-
inner: Mutex::new(CountedOwnedTasksInner {
79-
list: CountedLinkedList::new(),
80-
closed: false,
81-
}),
81+
list: List::new(shard_size),
82+
closed: AtomicBool::new(false),
8283
id: get_next_id(),
8384
}
8485
}
@@ -112,24 +113,23 @@ impl<S: 'static> OwnedTasks<S> {
112113
task.header().set_owner_id(self.id);
113114
}
114115

115-
let mut lock = self.inner.lock();
116-
if lock.closed {
117-
drop(lock);
118-
drop(notified);
116+
let shard = self.list.lock_shard(&task);
117+
// Check the closed flag in the lock for ensuring all that tasks
118+
// will shut down after the OwnedTasks has been closed.
119+
if self.closed.load(Ordering::Acquire) {
120+
drop(shard);
119121
task.shutdown();
120-
None
121-
} else {
122-
lock.list.push_front(task);
123-
Some(notified)
122+
return None;
124123
}
124+
shard.push(task);
125+
Some(notified)
125126
}
126127

127128
/// Asserts that the given task is owned by this OwnedTasks and convert it to
128129
/// a LocalNotified, giving the thread permission to poll this task.
129130
#[inline]
130131
pub(crate) fn assert_owner(&self, task: Notified<S>) -> LocalNotified<S> {
131132
debug_assert_eq!(task.header().get_owner_id(), Some(self.id));
132-
133133
// safety: All tasks bound to this OwnedTasks are Send, so it is safe
134134
// to poll it on this thread no matter what thread we are on.
135135
LocalNotified {
@@ -140,34 +140,34 @@ impl<S: 'static> OwnedTasks<S> {
140140

141141
/// Shuts down all tasks in the collection. This call also closes the
142142
/// collection, preventing new items from being added.
143-
pub(crate) fn close_and_shutdown_all(&self)
143+
///
144+
/// The parameter start determines which shard this method will start at.
145+
/// Using different values for each worker thread reduces contention.
146+
pub(crate) fn close_and_shutdown_all(&self, start: usize)
144147
where
145148
S: Schedule,
146149
{
147-
// The first iteration of the loop was unrolled so it can set the
148-
// closed bool.
149-
let first_task = {
150-
let mut lock = self.inner.lock();
151-
lock.closed = true;
152-
lock.list.pop_back()
153-
};
154-
match first_task {
155-
Some(task) => task.shutdown(),
156-
None => return,
150+
self.closed.store(true, Ordering::Release);
151+
for i in start..self.get_shard_size() + start {
152+
loop {
153+
let task = self.list.pop_back(i);
154+
match task {
155+
Some(task) => {
156+
task.shutdown();
157+
}
158+
None => break,
159+
}
160+
}
157161
}
162+
}
158163

159-
loop {
160-
let task = match self.inner.lock().list.pop_back() {
161-
Some(task) => task,
162-
None => return,
163-
};
164-
165-
task.shutdown();
166-
}
164+
#[inline]
165+
pub(crate) fn get_shard_size(&self) -> usize {
166+
self.list.shard_size()
167167
}
168168

169169
pub(crate) fn active_tasks_count(&self) -> usize {
170-
self.inner.lock().list.count()
170+
self.list.len()
171171
}
172172

173173
pub(crate) fn remove(&self, task: &Task<S>) -> Option<Task<S>> {
@@ -179,11 +179,27 @@ impl<S: 'static> OwnedTasks<S> {
179179

180180
// safety: We just checked that the provided task is not in some other
181181
// linked list.
182-
unsafe { self.inner.lock().list.remove(task.header_ptr()) }
182+
unsafe { self.list.remove(task.header_ptr()) }
183183
}
184184

185185
pub(crate) fn is_empty(&self) -> bool {
186-
self.inner.lock().list.is_empty()
186+
self.list.is_empty()
187+
}
188+
189+
/// Generates the size of the sharded list based on the number of worker threads.
190+
///
191+
/// The sharded lock design can effectively alleviate
192+
/// lock contention performance problems caused by high concurrency.
193+
///
194+
/// However, as the number of shards increases, the memory continuity between
195+
/// nodes in the intrusive linked list will diminish. Furthermore,
196+
/// the construction time of the sharded list will also increase with a higher number of shards.
197+
///
198+
/// Due to the above reasons, we set a maximum value for the shared list size,
199+
/// denoted as `MAX_SHARED_LIST_SIZE`.
200+
fn gen_shared_list_size(num_cores: usize) -> usize {
201+
const MAX_SHARED_LIST_SIZE: usize = 1 << 16;
202+
usize::min(MAX_SHARED_LIST_SIZE, num_cores.next_power_of_two() * 4)
187203
}
188204
}
189205

@@ -192,9 +208,9 @@ cfg_taskdump! {
192208
/// Locks the tasks, and calls `f` on an iterator over them.
193209
pub(crate) fn for_each<F>(&self, f: F)
194210
where
195-
F: FnMut(&Task<S>)
211+
F: FnMut(&Task<S>),
196212
{
197-
self.inner.lock().list.for_each(f)
213+
self.list.for_each(f);
198214
}
199215
}
200216
}

tokio/src/runtime/task/mod.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,7 @@ cfg_taskdump! {
208208

209209
use crate::future::Future;
210210
use crate::util::linked_list;
211+
use crate::util::sharded_list;
211212

212213
use std::marker::PhantomData;
213214
use std::ptr::NonNull;
@@ -503,3 +504,16 @@ unsafe impl<S> linked_list::Link for Task<S> {
503504
self::core::Trailer::addr_of_owned(Header::get_trailer(target))
504505
}
505506
}
507+
508+
/// # Safety
509+
///
510+
/// The id of a task is never changed after creation of the task, so the return value of
511+
/// `get_shard_id` will not change. (The cast may throw away the upper 32 bits of the task id, but
512+
/// the shard id still won't change from call to call.)
513+
unsafe impl<S> sharded_list::ShardedListItem for Task<S> {
514+
unsafe fn get_shard_id(target: NonNull<Self::Target>) -> usize {
515+
// SAFETY: The caller guarantees that `target` points at a valid task.
516+
let task_id = unsafe { Header::get_id(target) };
517+
task_id.0 as usize
518+
}
519+
}

0 commit comments

Comments
 (0)