Skip to content

Commit f82bcf3

Browse files
committed
Merge 'tokio-1.51.2' into 'tokio-1.52.x' (#8114)
2 parents 7db9bc4 + 64834ec commit f82bcf3

11 files changed

Lines changed: 64 additions & 349 deletions

File tree

tokio/CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,14 @@
5050
[#8035]: https://github.com/tokio-rs/tokio/pull/8035
5151
[#8040]: https://github.com/tokio-rs/tokio/pull/8040
5252

53+
# 1.51.2 (May 4th, 2026)
54+
55+
This release reverts the LIFO slot stealing change introduced in 1.51.0
56+
([#7431]), due to [its performance impact][#8065]. ([#8100])
57+
58+
[#8065]: https://github.com/tokio-rs/tokio/pull/8065
59+
[#8100]: https://github.com/tokio-rs/tokio/pull/8100
60+
5361
# 1.51.1 (April 8th, 2026)
5462

5563
### Fixed

tokio/src/runtime/builder.rs

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1298,21 +1298,22 @@ impl Builder {
12981298
/// scheduled task being polled first.
12991299
///
13001300
/// To implement this heuristic, each worker thread has a slot which
1301-
/// holds the task that should be polled next. In earlier versions of
1302-
/// Tokio, this slot could not be stolen by other worker threads, which
1303-
/// can result in lower total throughput when tasks tend to have longer
1304-
/// poll times.
1301+
/// holds the task that should be polled next. However, this slot cannot
1302+
/// be stolen by other worker threads, which can result in lower total
1303+
/// throughput when tasks tend to have longer poll times.
13051304
///
13061305
/// This configuration option will disable this heuristic resulting in
1307-
/// all scheduled tasks being pushed into the worker-local queue. This
1308-
/// was intended as a workaround for the LIFO slot not being stealable.
1309-
/// As of Tokio 1.51, tasks can be stolen from the LIFO slot. In a
1310-
/// future version, this option may be deprecated.
1306+
/// all scheduled tasks being pushed into the worker-local queue, which
1307+
/// is stealable.
1308+
///
1309+
/// Consider trying this option when the task "scheduled" time is high
1310+
/// but the runtime is underutilized. Use [tokio-rs/tokio-metrics] to
1311+
/// collect this data.
13111312
///
13121313
/// # Unstable
13131314
///
1314-
/// This configuration option was considered a workaround for the LIFO
1315-
/// slot not being stealable. Since this is no longer the case, we will
1315+
/// This configuration option is considered a workaround for the LIFO
1316+
/// slot not being stealable. When the slot becomes stealable, we will
13161317
/// revisit whether or not this option is necessary. See
13171318
/// issue [tokio-rs/tokio#4941].
13181319
///

tokio/src/runtime/config.rs

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,11 @@ pub(crate) struct Config {
3434

3535
/// The multi-threaded scheduler includes a per-worker LIFO slot used to
3636
/// store the last scheduled task. This can improve certain usage patterns,
37-
/// especially message passing between tasks.
37+
/// especially message passing between tasks. However, this LIFO slot is not
38+
/// currently stealable.
3839
///
39-
/// In Tokio versions before 1.51, tasks in the LIFO slot could not be
40-
/// stolen, which could cause issues in applications with long poll times.
41-
/// As a stop-gap, this unstable option lets users disable the LIFO task.
42-
/// Now that the LIFO slot is stealable, we may remove this option in a
43-
/// future version.
40+
/// Eventually, the LIFO slot **will** become stealable, however as a
41+
/// stop-gap, this unstable option lets users disable the LIFO task.
4442
pub(crate) disable_lifo_slot: bool,
4543

4644
/// Random number generator seed to configure runtimes to act in a

tokio/src/runtime/mod.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -367,8 +367,8 @@
367367
//! three times in a row, it is temporarily disabled until the worker thread has
368368
//! scheduled a task that didn't come from the lifo slot. The lifo slot can be
369369
//! disabled using the [`disable_lifo_slot`] setting. The lifo slot is separate
370-
//! from the local queue, and is stolen from by other worker threads only when
371-
//! a worker's local queue has been drained.
370+
//! from the local queue, so other worker threads cannot steal the task in the
371+
//! lifo slot.
372372
//!
373373
//! When a task is woken from a thread that is not a worker thread, then the
374374
//! task is placed in the global queue.

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

Lines changed: 4 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -52,13 +52,6 @@ pub(crate) struct Inner<T: 'static> {
5252
/// Only updated by producer thread but read by many threads.
5353
tail: AtomicUnsignedShort,
5454

55-
/// When a task is scheduled from a worker, it is stored in this slot. The
56-
/// worker will check this slot for a task **before** checking the run
57-
/// queue. This effectively results in the **last** scheduled task to be run
58-
/// next (LIFO). This is an optimization for improving locality which
59-
/// benefits message passing patterns and helps to reduce latency.
60-
lifo: task::AtomicNotified<T>,
61-
6255
/// Elements
6356
buffer: Box<[UnsafeCell<MaybeUninit<task::Notified<T>>>; LOCAL_QUEUE_CAPACITY]>,
6457
}
@@ -99,7 +92,6 @@ pub(crate) fn local<T: 'static>() -> (Steal<T>, Local<T>) {
9992
let inner = Arc::new(Inner {
10093
head: AtomicUnsignedLong::new(0),
10194
tail: AtomicUnsignedShort::new(0),
102-
lifo: task::AtomicNotified::empty(),
10395
buffer: make_fixed_size(buffer.into_boxed_slice()),
10496
});
10597

@@ -116,10 +108,9 @@ impl<T> Local<T> {
116108
/// Returns the number of entries in the queue
117109
pub(crate) fn len(&self) -> usize {
118110
let (_, head) = unpack(self.inner.head.load(Acquire));
119-
let lifo = self.inner.lifo.is_some() as usize;
120111
// safety: this is the **only** thread that updates this cell.
121112
let tail = unsafe { self.inner.tail.unsync_load() };
122-
len(head, tail) + lifo
113+
len(head, tail)
123114
}
124115

125116
/// How many tasks can be pushed into the queue
@@ -410,28 +401,14 @@ impl<T> Local<T> {
410401

411402
Some(self.inner.buffer[idx].with(|ptr| unsafe { ptr::read(ptr).assume_init() }))
412403
}
413-
414-
/// Pushes a task to the LIFO slot, returning the task previously in the
415-
/// LIFO slot (if there was one).
416-
pub(crate) fn push_lifo(&self, task: task::Notified<T>) -> Option<task::Notified<T>> {
417-
self.inner.lifo.swap(Some(task))
418-
}
419-
420-
/// Pops the task currently held in the LIFO slot, if there is one;
421-
/// otherwise, returns `None`.
422-
pub(crate) fn pop_lifo(&self) -> Option<task::Notified<T>> {
423-
// LIFO-suction!
424-
self.inner.lifo.take()
425-
}
426404
}
427405

428406
impl<T> Steal<T> {
429407
/// Returns the number of entries in the queue
430408
pub(crate) fn len(&self) -> usize {
431409
let (_, head) = unpack(self.0.head.load(Acquire));
432410
let tail = self.0.tail.load(Acquire);
433-
let lifo = self.0.lifo.is_some() as usize;
434-
len(head, tail) + lifo
411+
len(head, tail)
435412
}
436413

437414
/// Return true if the queue is empty,
@@ -466,14 +443,8 @@ impl<T> Steal<T> {
466443
let mut n = self.steal_into2(dst, dst_tail);
467444

468445
if n == 0 {
469-
// If no tasks were stolen, let's see if there's one in the LIFO
470-
// slot.
471-
let lifo = self.0.lifo.take();
472-
if lifo.is_some() {
473-
dst_stats.incr_steal_count(1);
474-
dst_stats.incr_steal_operations();
475-
}
476-
return lifo;
446+
// No tasks were stolen
447+
return None;
477448
}
478449

479450
dst_stats.incr_steal_count(n as u16);
@@ -605,7 +576,6 @@ impl<T> Drop for Local<T> {
605576
fn drop(&mut self) {
606577
if !std::thread::panicking() {
607578
assert!(self.pop().is_none(), "queue not empty");
608-
assert!(self.pop_lifo().is_none(), "LIFO slot not empty");
609579
}
610580
}
611581
}

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

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,13 @@ struct Core {
112112
/// Used to schedule bookkeeping tasks every so often.
113113
tick: u32,
114114

115+
/// When a task is scheduled from a worker, it is stored in this slot. The
116+
/// worker will check this slot for a task **before** checking the run
117+
/// queue. This effectively results in the **last** scheduled task to be run
118+
/// next (LIFO). This is an optimization for improving locality which
119+
/// benefits message passing patterns and helps to reduce latency.
120+
lifo_slot: Option<Notified>,
121+
115122
/// When `true`, locally scheduled tasks go to the LIFO slot. When `false`,
116123
/// they go to the back of the `run_queue`.
117124
lifo_enabled: bool,
@@ -282,6 +289,7 @@ pub(super) fn create(
282289

283290
cores.push(Box::new(Core {
284291
tick: 0,
292+
lifo_slot: None,
285293
lifo_enabled: !config.disable_lifo_slot,
286294
run_queue,
287295
#[cfg(all(tokio_unstable, feature = "time"))]
@@ -443,7 +451,7 @@ where
443451
// If we heavily call `spawn_blocking`, there might be no available thread to
444452
// run this core. Except for the task in the lifo_slot, all tasks can be
445453
// stolen, so we move the task out of the lifo_slot to the run_queue.
446-
if let Some(task) = core.run_queue.pop_lifo() {
454+
if let Some(task) = core.lifo_slot.take() {
447455
core.run_queue
448456
.push_back_or_overflow(task, &*cx.worker.handle, &mut core.stats);
449457
}
@@ -696,7 +704,7 @@ impl Context {
696704
};
697705

698706
// Check for a task in the LIFO slot
699-
let task = match core.run_queue.pop_lifo() {
707+
let task = match core.lifo_slot.take() {
700708
Some(task) => task,
701709
None => {
702710
self.reset_lifo_enabled(&mut core);
@@ -1122,7 +1130,7 @@ impl Core {
11221130
}
11231131

11241132
fn next_local_task(&mut self) -> Option<Notified> {
1125-
self.run_queue.pop_lifo().or_else(|| self.run_queue.pop())
1133+
self.lifo_slot.take().or_else(|| self.run_queue.pop())
11261134
}
11271135

11281136
/// Function responsible for stealing tasks from another worker
@@ -1178,7 +1186,7 @@ impl Core {
11781186
}
11791187

11801188
fn has_tasks(&self) -> bool {
1181-
self.run_queue.has_tasks()
1189+
self.lifo_slot.is_some() || self.run_queue.has_tasks()
11821190
}
11831191

11841192
fn should_notify_others(&self) -> bool {
@@ -1187,7 +1195,7 @@ impl Core {
11871195
if self.is_searching {
11881196
return false;
11891197
}
1190-
self.run_queue.len() > 1
1198+
self.lifo_slot.is_some() as usize + self.run_queue.len() > 1
11911199
}
11921200

11931201
/// Prepares the worker state for parking.
@@ -1349,23 +1357,29 @@ impl Handle {
13491357
// task must always be pushed to the back of the queue, enabling other
13501358
// tasks to be executed. If **not** a yield, then there is more
13511359
// flexibility and the task may go to the front of the queue.
1352-
if is_yield || !core.lifo_enabled {
1360+
let should_notify = if is_yield || !core.lifo_enabled {
13531361
core.run_queue
13541362
.push_back_or_overflow(task, self, &mut core.stats);
1363+
true
13551364
} else {
13561365
// Push to the LIFO slot
1357-
if let Some(prev) = core.run_queue.push_lifo(task) {
1358-
// There was a previous task in the LIFO slot which needs
1359-
// to be pushed to the back of the run queue.
1366+
let prev = core.lifo_slot.take();
1367+
let ret = prev.is_some();
1368+
1369+
if let Some(prev) = prev {
13601370
core.run_queue
13611371
.push_back_or_overflow(prev, self, &mut core.stats);
13621372
}
1373+
1374+
core.lifo_slot = Some(task);
1375+
1376+
ret
13631377
};
13641378

13651379
// Only notify if not currently parked. If `park` is `None`, then the
13661380
// scheduling is from a resource driver. As notifications often come in
13671381
// batches, the notification is delayed until the park is complete.
1368-
if core.park.is_some() {
1382+
if should_notify && core.park.is_some() {
13691383
self.notify_parked_local();
13701384
}
13711385
}

tokio/src/runtime/task/atomic_notified.rs

Lines changed: 0 additions & 58 deletions
This file was deleted.

tokio/src/runtime/task/mod.rs

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -209,11 +209,6 @@ pub(crate) use self::raw::RawTask;
209209
mod state;
210210
use self::state::State;
211211

212-
#[cfg(feature = "rt-multi-thread")]
213-
mod atomic_notified;
214-
#[cfg(feature = "rt-multi-thread")]
215-
pub(crate) use self::atomic_notified::AtomicNotified;
216-
217212
mod waker;
218213

219214
pub(crate) use self::spawn_location::SpawnLocation;

0 commit comments

Comments
 (0)