We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Async From First Principles step 15 of 25
Hand-rolling `select`: racing futures and dropping the loser
join waits for both. select takes whichever finishes first and throws the
other one away. That second half is where async Rust stops being “sequential
code with extra keywords” and starts having semantics you have not met before.
pub enum Either<L, R> { Left(L), Right(R) }
pub struct Select<A: Future, B: Future> { .. }
pub fn race(a_pends: u32, b_pends: u32, biased: bool) -> (String, Vec<char>)
race builds the same tagged Countdown pair as the previous item, races
them, and returns the winner formatted as "a:10" or "b:20" plus the poll
log.
The polling order is part of the specification, because with equal pends
the winner is decided entirely by who is polled first:
-
biased == true— branch A is polled first on every poll. -
biased == false— the branches take turns: A first on the first poll, B first on the second, A first on the third, and so on.
So race(1, 1, true) is ("a:10", ['a','b','a']) and race(1, 1, false) is
("b:20", ['a','b','b']). Same futures, same number of pends, different
winner. A select without a documented order is not a specification, it is a
coin flip.
Fairness
Always polling the first branch first is the simplest thing that works, and it
starves the second branch whenever the first is usually ready. That is a
real production failure: a select! between “receive a message” and “shut
down” where messages always arrive means the shutdown branch is checked but
never wins.
Real select! macros randomise branch order by default for exactly this
reason, and tokio’s offers a biased; mode to opt out when you want
determinism and know what you are doing. A hand-rolled version cannot
randomise and stay testable, so this one alternates — deterministic, and fair
in the sense that neither branch is permanently at the back of the queue.
The loser
When one branch wins, Select returns Ready and is then dropped by whoever
owned it. Dropping it drops both children, including the one that was
halfway through its work. There is no notification, no cancel() call, no
chance to finish. Whatever partial state that future had accumulated —
half-parsed bytes, a partially built response, an item taken off a queue —
goes away with it.
That is the origin of the cancel-safety problem, and it is the single
sharpest edge in async Rust. If a future keeps its partial progress inside
itself, losing a select race loses that progress silently. If it keeps
partial progress in a longer-lived object it merely borrows, losing the race
costs nothing. That is why tokio documents cancel safety per method rather
than as a blanket rule. Item 16.22 goes into it properly; for now, notice that
the mechanism is nothing more exotic than Drop.
Shape notes
Select needs no manual Unpin impl — unlike Join it has no
Option<A::Output> field, so every field is already Unpin and the struct
gets it for free. Worth noticing rather than cargo-culting the impl from the
previous item.
clippy::type_complexity is a default-on lint and generic combinator types
get long fast; a type alias is the usual answer if you trip it.
Stuck?
Rust reference solution
Sign in to attempt this problem and reveal the reference solution.