Racing With Select
join! waits for everything. Sometimes you want the opposite: whichever of several things happens first, and none of the rest.
tokio::select! polls every branch and completes as soon as one of them does.
tokio::select! {
value = fetch_from_cache() => use_it(value),
value = fetch_from_disk() => use_it(value),
}Each arm is pattern = future => expression. Every arm must produce the same type, exactly like a match.
Every loser is cancelled
When one branch wins, the others are dropped mid-flight. Whatever they had done so far is thrown away.
That makes select! in a loop a sharp tool. A future that is safe to drop half-finished and poll again from scratch is called cancel safe. Receiver::recv is: a message is either taken or still in the channel, never lost in between. Reading half a line from a socket into a local buffer is not.
If several branches are ready at once, select! picks between them at random, so never write code that depends on branch order.
Reusing a future across iterations
A future created inside a loop body starts over on every pass. For a deadline that is exactly wrong: the timer resets after each message and never fires. Create it once, pin it, then poll the same one with &mut.
let deadline = sleep(limit);
tokio::pin!(deadline);
loop {
tokio::select! {
v = rx.recv() => { /* ... */ }
_ = &mut deadline => break,
}
}tokio::pin! pins the future in place on the stack, which is what lets you take &mut of it.
Your Task
Implement first_of
pub async fn first_of<A, B>(a: A, b: B) -> String
where
A: Future<Output = String>,
B: Future<Output = String>,Return whichever future produces a value first, and drop the other.
Implement collect_until_deadline
pub async fn collect_until_deadline(
rx: &mut mpsc::Receiver<i32>,
limit: Duration,
) -> Vec<i32>Collect messages in arrival order and stop at whichever comes first: the channel closing, or limit elapsing since the call started.
A producer sending steadily forever must not keep the collection running past limit.
Notes
- The deadline is measured from the start of the call, not from the last message.
rx.recv()yieldsNonewhen every sender is gone. That ends the loop too.- Both branches of a
select!arm must return the same type, so do the work in a block.
Hints
Click here to reveal hints
first_ofis oneselect!with two arms, each binding a value and returning it.- In
collect_until_deadline, build the sleep andtokio::pin!it before the loop. - Match on the
Optionthe receive arm produces: push onSome,breakonNone. - The deadline arm ignores its output and just breaks.
- If a test hangs, the deadline is being created inside the loop instead of outside it.
Run to execute your code and see test results here.