Theme
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
Dropto perform awaited network or storage cleanup. - Dropping a
JoinHandleand 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
.awaita useful mental model?
Related
- Q066. How does
JoinHandlebehave on completion, panic, drop, and abort? - Q068. How does
tokio::select!work, and what happens to losing branches? - Q070. What does cancellation safety mean?
References
- Async Book
- Tokio Documentation
- Rust Standard Library