Rust Tokio Cancellation
Cancellation is stopping async work before it finishes. Dropping a future cancels it.
One flag, many holders
Every clone of a token watches the same flag โ cancel once, and all of them hear it:
let token = CancellationToken::new();
let worker_token = token.clone();Watch the token in select!
The standard shape: your work races the token, and the token branch tidies up:
tokio::select! {
_ = token.cancelled() => println!("stop signal received"),
_ = sleep(Duration::from_secs(60)) => println!("work finished"),
}Child tokens stop one branch, not the tree
A child hears its parent, never the other way round:
let parent = CancellationToken::new();
let child = parent.child_token();
child.cancel();
println!("child cancelled = {}", child.is_cancelled());
println!("parent cancelled = {}", parent.is_cancelled());
let child2 = parent.child_token();
parent.cancel();
println!("second child cancelled = {}", child2.is_cancelled());Run a future under a token
run_until_cancelled folds the select! shape into one call โ Some for finished, None for stopped:
let done = token.run_until_cancelled(async { 7 }).await;
println!("finished = {done:?}");
token2.cancel();
let stopped = token2.run_until_cancelled(sleep(Duration::from_secs(60))).await;
println!("stopped = {stopped:?}");A guard cancels when it drops
drop_guard ties the token to a value's life. It takes the token itself, so keep a clone for watching:
let token = CancellationToken::new();
let watching = token.clone();
let guard = token.drop_guard();
drop(guard);
println!("cancelled = {}", watching.is_cancelled());Example
// Cargo.toml: tokio = { version = "1", features = ["full"] }, tokio-util = "0.7"
use tokio::time::{sleep, Duration};
use tokio_util::sync::CancellationToken;
#[tokio::main]
async fn main() {
let token = CancellationToken::new();
let worker_token = token.clone();
let worker = tokio::spawn(async move {
loop {
tokio::select! {
_ = worker_token.cancelled() => {
println!("worker: stop signal received, tidying up");
break;
}
_ = sleep(Duration::from_millis(20)) => {
println!("worker: tick");
}
}
}
});
sleep(Duration::from_millis(55)).await;
token.cancel();
worker.await.unwrap();
println!("shutdown complete");
}Key points
- Dropping a future cancels it at its next .await point โ there is no kill switch, and none is needed
- JoinHandle::abort() asks a spawned task to stop.
- Await the handle to learn that it was cancelled.
- tokio::signal::ctrl_c().await waits for the user to press Ctrl+C โ the classic shutdown trigger
- Graceful shutdown means: stop taking new work, give current work a short deadline to finish, then exit
- The Tokio tutorial's servers use a different pair of channels for the same job.
- One is a broadcast channel, used as the stop signal.
- The other is an mpsc channel.
- Its closing proves every task has ended.
- Cancellation is co-operative.
In this example, a worker loops with select!: it either sleeps 20 milliseconds and prints a tick, or it hears the cancellation token. After about 55 milliseconds, main cancels the token. The worker's next round takes the cancelled branch, prints its tidy-up line, and breaks the loop. Main awaits the handle, so the shutdown is fully finished before the program exits โ polite, complete, and controlled.
Note
A task that never awaits โ a tight loop โ cannot be cancelled until it reaches an .await.
Try it Yourself
Edit the code, then press Run.
Running is simulated ยท Rust Playground
Exercise
How do you stop one spawned task from another part of the program?