tokio::select!
is quite different from futures::select!
. One notable difference is that it doesn't require the polled futures to implement FusedFuture
. This allows code like this to compile:
use tokio::{select, time::{interval, delay_for}};
use std::time::Duration;
async fn some_action() -> u64 {
delay_for(Duration::from_secs(4)).await;
42
}
#[tokio::main]
async fn main() {
let mut interval = interval(Duration::from_secs(1));
let mut some_action = Box::pin(some_action());
loop {
select! {
result = &mut some_action => {
println!("got result: {:?}", result);
}
_ = interval.tick() => {
println!("tick!");
}
}
}
}
This code panics at runtime: (playground)
thread 'main' panicked at '`async fn` resumed after completion', src/main.rs:4:31
The code is buggy, and it may not be apparent. Some code with a similar bug could only panic in rare cases like timeouts, so covering it with tests is really hard.
The futures::select!
macro requires FusedFuture
, so this whole class of bugs is checked at compile time. The compiler will force you to fuse everything, and no panic will occur.
It's unclear how I could use extra features of tokio::select!
to avoid the bug. I can't use a pattern match, since the future just returns a plain value. I can't use an arm guard because I don't have any suitable variables. I could introduce a variable and track the status of the future in it, but that's like implementing .fuse()
in a very inconvenient, error-prone way. I could just fuse everything, but then I could just use futures::select!
which checks that at compile time.
I'd like to discuss an example provided in the docs. The text above the example says:
Collect the contents of two streams. In this example, we rely on pattern matching and the fact that
stream::iter
is "fused", i.e. once the stream is complete, all calls tonext()
returnNone
.
Note the implicit notion of the fact that stream::iter
is fused. If it is replaced with a non-fused stream, it would still be polled after returning Ready(None)
and cause bad behavior at runtime (playground). The change of the stream implementation could happen in another place, where the connection to select!
invokation is not apparent, causing a bug that can't be detected locally. More than that, there is no apparent way to fix that example to work with non-fused streams. Pattern matching is already used and doesn't help, and tracking the state of each stream with extra variables is, again, inconvenient and error-prone. If you can't really use tokio::select!
on non-fused streams, the lack of a FusedFuture
trait bound doesn't really provide any benefits.
The description of the pull request that added tokio::select!
does not answer my questions. I don't understand the rationale behind that. Yes, fusing futures is inconvenient, but it's necessary if you want to safely poll them in an uncontrolled manner, like in a select!
macro. I could see tokio::select!
being necessary in some rare cases where you really can't afford to fuse a future, but providing it as the default general purpose implementation of select!
seems like a massive footgun to me.