We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Async From First Principles step 21 of 25
Cancellation is just: stop polling and drop
Rust has no future.cancel(). No cancellation exception, no signal, no token
you have to remember to check. Cancelling a future is dropping it, and that is
the entire API.
pub struct Guard { /* logs "drop:<name>" when dropped */ }
pub async fn body(log: Log);
pub fn cancel_log(polls_before_cancel: u32) -> Vec<String>
cancel_log drives body for at most polls_before_cancel polls; if it has
not finished, it appends "cancelling" and drops it. body writes
"started", creates a guard named inner, awaits, writes "resumed",
creates a guard named late, awaits, writes "finished".
cancel_log(0) == ["cancelling"]
cancel_log(1) == ["started", "cancelling", "drop:inner"]
cancel_log(3) == ["started", "resumed", "cancelling", "drop:late", "drop:inner"]
cancel_log(5) == ["started", "resumed", "finished", "drop:late", "drop:inner"]
Four facts are visible in those four lines.
Zero polls means nothing ran. Not “started and then stopped” — the body never executed a single statement. Laziness again, from item 16.2.
Cancellation runs destructors. The future owns its state machine, and the
locals live across an .await are fields of it. Dropping the future drops them,
so Drop fires, so your RAII cleanup happens. This is why async Rust needs no
finally: the same mechanism that closes your files in synchronous code closes
them when a task is cancelled.
Drop order is reverse declaration order. late is created second and
dropped first. That is not an async rule; it is the ordinary Rust rule, and it
applies unchanged inside a suspended state machine.
Unreached statements leave no trace. cancel_log(3) never writes
"finished" and never creates anything after the second await. Cancelled work
is not partially undone — it simply never happened.
The part people get wrong
Cancellation is not preemptive. A future can only be cancelled at an
.await point, because that is the only place poll returns and control
comes back to whoever might drop it. Between two awaits, a future is
uninterruptible. A CPU loop, a thread::sleep, a blocking read() — none of
them can be cancelled, no matter how urgently the caller wants it. There is no
mechanism that could do it: nobody is running who could act.
Compare with a thread. You cannot safely kill a thread either, which is why
std::thread has no kill. Async cancellation is strictly better than that —
it is synchronous, immediate, and deterministic — but only at the points where
the future has chosen to yield.
The observation trap
The log lives in an Rc<RefCell<Vec<String>>> that both the caller and the
future hold. Read it while the future is still alive and you see the state
before its destructors run. You have to drop the future first:
drop(fut);
Rc::into_inner(log).unwrap().into_inner()
Rc::into_inner also documents the invariant: it returns Some only if this
was the last handle, so it fails loudly if the future is somehow still around.
That is the bug shipped in the starter — it reads the log one line too early
and loses every drop: entry.
Two neighbours
std::mem::forget(fut) is safe and skips every destructor: no drop:
lines at all, and if the guard were closing a file the file would stay open.
Leaking is safe in Rust; use-after-free is not. That is a deliberate and
sometimes surprising line.
clippy::let_underscore_future (restriction group, off by default) exists
because let _ = some_future(); drops the future immediately — which, now
that you have read this far, you can see is a complete cancellation of work
the author almost certainly intended to happen.
Loading visualization…
Stuck?
Rust reference solution
Sign in to attempt this problem and reveal the reference solution.