mirror of
https://github.com/tokio-rs/tokio.git
synced 2026-08-23 00:00:10 +02:00
threadpool: drop incomplete tasks on shutdown (#722)
## Motivation When the thread pool shuts down, futures that have been polled at least once but not completed yet are simply leaked. We should drop them instead. ## Solution Multiple changes are introduced: * Tasks are assigned a home worker the first time they are polled. * Each worker contains a set of tasks (`Arc<Task>`) it is home to. When a task is assigned a home worker, it is registered in that worker's set of tasks. When the task is completed, it is unregistered from the set. * When the thread pool shuts down and after all worker threads stop, the remaining tasks in workers' sets are aborted, i.e. they are switched to the `Aborted` state and their `Future`s are dropped. * The thread pool shutdown process is refactored to make it more robust. We don't track the number of active threads manually anymore. Instead, there's `Arc<ShutdownTrigger>` that aborts remaining tasks and completes the `Shutdown` future once it gets destroyed (when all `Worker`s and `ThreadPool` get dropped because they're the only ones to contain strong references to the `ShutdownTrigger`). Closes #424 Closes #428
This commit is contained in:
@@ -15,10 +15,10 @@ use futures::{self, Future, Async};
|
||||
use futures::executor::{self, Spawn};
|
||||
|
||||
use std::{fmt, panic, ptr};
|
||||
use std::cell::{UnsafeCell};
|
||||
use std::cell::{Cell, UnsafeCell};
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicUsize, AtomicPtr};
|
||||
use std::sync::atomic::Ordering::{AcqRel, Release, Relaxed};
|
||||
use std::sync::atomic::Ordering::{AcqRel, Acquire, Release, Relaxed};
|
||||
|
||||
/// Harness around a future.
|
||||
///
|
||||
@@ -34,6 +34,21 @@ pub(crate) struct Task {
|
||||
/// Next pointer in the queue of tasks pending blocking capacity.
|
||||
next_blocking: AtomicPtr<Task>,
|
||||
|
||||
/// ID of the worker that polled this task first.
|
||||
///
|
||||
/// This field can be a `Cell` because it's only accessed by the worker thread that is
|
||||
/// executing the task.
|
||||
///
|
||||
/// The worker ID is represented by a `u32` rather than `usize` in order to save some space
|
||||
/// on 64-bit platforms.
|
||||
pub reg_worker: Cell<Option<u32>>,
|
||||
|
||||
/// The key associated with this task in the `Slab` it was registered in.
|
||||
///
|
||||
/// This field can be a `Cell` because it's only accessed by the worker thread that has
|
||||
/// registered the task.
|
||||
pub reg_index: Cell<usize>,
|
||||
|
||||
/// Store the future at the head of the struct
|
||||
///
|
||||
/// The future is dropped immediately when it transitions to Complete
|
||||
@@ -61,6 +76,8 @@ impl Task {
|
||||
state: AtomicUsize::new(State::new().into()),
|
||||
blocking: AtomicUsize::new(BlockingState::new().into()),
|
||||
next_blocking: AtomicPtr::new(ptr::null_mut()),
|
||||
reg_worker: Cell::new(None),
|
||||
reg_index: Cell::new(0),
|
||||
future: UnsafeCell::new(Some(task_fut)),
|
||||
}
|
||||
}
|
||||
@@ -75,6 +92,8 @@ impl Task {
|
||||
state: AtomicUsize::new(State::stub().into()),
|
||||
blocking: AtomicUsize::new(BlockingState::new().into()),
|
||||
next_blocking: AtomicPtr::new(ptr::null_mut()),
|
||||
reg_worker: Cell::new(None),
|
||||
reg_index: Cell::new(0),
|
||||
future: UnsafeCell::new(Some(task_fut)),
|
||||
}
|
||||
}
|
||||
@@ -166,6 +185,41 @@ impl Task {
|
||||
}
|
||||
}
|
||||
|
||||
/// Aborts this task.
|
||||
///
|
||||
/// This is called when the threadpool shuts down and the task has already beed polled but not
|
||||
/// completed.
|
||||
pub fn abort(&self) {
|
||||
use self::State::*;
|
||||
|
||||
let mut state = self.state.load(Acquire).into();
|
||||
|
||||
loop {
|
||||
match state {
|
||||
Idle | Scheduled => {}
|
||||
Running | Notified | Complete | Aborted => {
|
||||
// It is assumed that no worker threads are running so the task must be either
|
||||
// in the idle or scheduled state.
|
||||
panic!("unexpected state while aborting task: {:?}", state);
|
||||
}
|
||||
}
|
||||
|
||||
let actual = self.state.compare_and_swap(
|
||||
state.into(),
|
||||
Aborted.into(),
|
||||
AcqRel).into();
|
||||
|
||||
if actual == state {
|
||||
// The future has been aborted. Drop it immediately to free resources and run drop
|
||||
// handlers.
|
||||
self.drop_future();
|
||||
break;
|
||||
}
|
||||
|
||||
state = actual;
|
||||
}
|
||||
}
|
||||
|
||||
/// Notify the task
|
||||
pub fn notify(me: Arc<Task>, pool: &Arc<Pool>) {
|
||||
if me.schedule() {
|
||||
@@ -206,7 +260,7 @@ impl Task {
|
||||
_ => return false,
|
||||
}
|
||||
}
|
||||
Complete | Notified | Scheduled => return false,
|
||||
Complete | Aborted | Notified | Scheduled => return false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,9 @@ pub(crate) enum State {
|
||||
|
||||
/// Task is complete
|
||||
Complete = 4,
|
||||
|
||||
/// Task was aborted because the thread pool has been shut down
|
||||
Aborted = 5,
|
||||
}
|
||||
|
||||
// ===== impl State =====
|
||||
@@ -39,7 +42,7 @@ impl From<usize> for State {
|
||||
|
||||
debug_assert!(
|
||||
src >= Idle as usize &&
|
||||
src <= Complete as usize, "actual={}", src);
|
||||
src <= Aborted as usize, "actual={}", src);
|
||||
|
||||
unsafe { ::std::mem::transmute(src) }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user