Skip to content

Q069. How are futures and tasks cancelled in Rust?

Short Answer

An in-progress future is normally cancelled by dropping it. A spawned Tokio task can be requested to cancel through JoinHandle::abort, runtime shutdown, or an application-level shutdown signal that lets it exit voluntarily. Dropping the future runs destructors for initialized fields, but Rust has no async destructor, so cleanup that requires .await must be designed explicitly.

Deep Dive

Drop-based cancellation is immediate from the future owner's perspective: the computation will not be polled again. It does not undo external effects already performed. A partially written socket message, sent request, or committed database operation remains externally visible according to that system's semantics.

Cooperative cancellation uses a signal, often a channel or cancellation token, that tasks observe in select!. This allows orderly async cleanup, flushing, and reporting before return. Forced cancellation through abort is useful as a deadline fallback, not as the only shutdown mechanism.

Resource destructors still close files, release locks, and free memory synchronously. Protocol-level cleanup that needs network I/O should have an explicit close or shutdown phase with a timeout.

Internal Model

The generated state machine knows which fields are initialized in its current state. Dropping it runs state-specific drop glue. Child futures and owned guards are dropped recursively.

Tokio abort marks a task cancelled and causes the runtime to drop its future at a scheduling boundary. Synchronous code within a poll cannot be interrupted in the middle.

Example

rust
use std::sync::{
    atomic::{AtomicBool, Ordering},
    Arc,
};
use std::time::Duration;

struct Dropped(Arc<AtomicBool>);

impl Drop for Dropped {
    fn drop(&mut self) {
        self.0.store(true, Ordering::Release);
    }
}

#[tokio::main]
async fn main() {
    let dropped = Arc::new(AtomicBool::new(false));
    let state = Arc::clone(&dropped);

    let future = async move {
        let _guard = Dropped(state);
        std::future::pending::<()>().await;
    };

    assert!(tokio::time::timeout(Duration::from_millis(1), future)
        .await
        .is_err());
    assert!(dropped.load(Ordering::Acquire));
}

Common Mistakes

  • Assuming cancellation rolls back all prior external side effects.
  • Expecting Drop to perform awaited network or storage cleanup.
  • Dropping a JoinHandle and calling that task cancellation.

Follow-ups

  • What cleanup is guaranteed when a future is dropped?
  • How can a service combine graceful cancellation with an abort deadline?
  • Why is cancellation at any .await a useful mental model?

References

  • Async Book
  • Tokio Documentation
  • Rust Standard Library