Never snooze a future
35 points by vinhnx 6 days ago | 7 comments

yoshuaw 11 hours ago
For the past seven years I've been urging people to stop using `select!` in async Rust code and use dedicated structured control flow primitives instead. Correct use of `select!` cannot be guaranteed by the compiler [1], and so it's unsurprising it leads to bugs.

For the first example the better option would be to use the `race` operation [2]. For the second it would be better to use `ConcurrentStream` [3]. Many (but not all) of the issues people have with cancellation in async Rust can be improved with the advice: "don't use select!".

[1]: https://blog.yoshuawuyts.com/futures-concurrency-3/#issues-w...

[2]: https://docs.rs/futures-concurrency/latest/futures_concurren...

[3]: https://docs.rs/futures-concurrency/latest/futures_concurren...

reply
cousin_it 12 hours ago
Looks like the code of foo() says "take a lock then sleep for 10 millis", but actually it can take the lock and then sleep forever, depending on how it's polled. Well! This seems like a bug with the async abstraction in Rust then. Or if you don't like "bug", then "disagreement with intuition that will cause bugs forever". Goroutines in Go don't have this problem: if a goroutine says it'll take a lock and sleep for 10 millis, then that's what it'll do. The reason is because Go concurrency is preemptive, while async in Rust is cooperative.

So maybe the lesson I'd take is that if you're programming with locks (or other synchronization primitives), your concurrency model has to be preemptive. Cooperative concurrency + locks = invitation to very subtle bugs.

reply
rcxdude 11 hours ago
I don't think it's preemptive vs cooperative that matters. What Rust's abstraction allows is for a function to act like a mini-executor itself, polling multiple other futures itself instead of delegating it to the runtime. That allows them to contain subtle issues like stopping polling a future without cancelling it, which is, yeah, dangerous if one of those futures can block other futures from running (another way you could come at this is to say that maybe holding locks across async points should be avoided).
reply
cousin_it 10 hours ago
> holding locks across async points should be avoided

Wait, what would be the point of using locks then? It seems to me there's no point taking a lock if you're gonna release it without calling any awaits, because nothing can interfere anyway. Or do you mean cases where you have both cooperative and preemptive concurrency in the same program?

reply
dmgl 9 hours ago
> It seems to me there's no point taking a lock if you're gonna release it without calling any awaits, because nothing can interfere anyway.

This is probably true only in single threaded executor. Other threads often exist.

reply
ixxie 13 hours ago
I thought this was a "carpe diem" motivational post xD
reply
nixpulvis 10 hours ago
Is the non-local reasoning here a limitation of Rust, async Rust, or the libraries built around async Rust?
reply