Never snooze a future (jacko.io)

by vinhnx 7 comments 40 points
Read article View on HN

7 comments

[−] yoshuaw 64d 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...

[−] cousin_it 64d 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.

[−] rcxdude 64d 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).
[−] cousin_it 64d 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?

[−] dmgl 64d 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.

[−] ixxie 64d ago
I thought this was a "carpe diem" motivational post xD
[−] nixpulvis 64d ago
Is the non-local reasoning here a limitation of Rust, async Rust, or the libraries built around async Rust?