🎓 A1Learner
🔍
0 of 45 lessons completed

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?