4 ms·
The complaints around async/await vs threads to my mind have not been that one is more or less complex than the other. It is that it bifurcates the ecosystem an
by zaphar 3y ago
The complaints around async/await vs threads to my mind have not been that one is more or less complex than the other. It is that it bifurcates the ecosystem and one of them ends up being a second class citizen causing friction when you choose the wrong one for your project.
While you can mix and match them it's hacky and inefficient when you need to. As it stands now the Rust ecosystem has decided that if you want to do anything involving IO you are stuck with an all async/await ecosystem. Since nearly everything you might want to do in Rust probably involves IO with very few exceptions that means for the most part you should probably ignore non-async libraries regardless of whether the rest of your application wants it to be async or not.
There is a hypothetical world where Rust used abstractions that are even more composable than async/await, whose composability really wants everything else to be async/await too. If that had happened then I think most of the complaints would have disappeared.
- HideousKojima 3y ago>As it stands now the Rust ecosystem has decided that if you want to do anything involving IO you are stuck with an all async/await ecosystem. I mean C# works basically the same way, even through there are non-async options for IO using the async options basically forces you to be async all the way back to Main(). There are ways to safely call async methods from sync methods but they make debugging infinitely harder.
- zaphar 3y agoWell, yes. That doesn't mean it's not annoying though. It happens in every language that provides syntactic support for the distinction between async/await and non async. It's, I think, core to the syntactic and semantic abstractions that were popularized by Javascript.
- K0nserv 3y agoI agree with your diagnosis. It's what I concluded in my own Rust async blog post[0](which surely are mandatory now). It's even worse than bifurcating the ecosystem because even within async code it's almost always closely tied to the executor, usually Tokio. I talk about this as an extension to function colouring, adopting without.boats's three colour proposition with blue(non-IO), green(blocking-IO), and red(async-IO). In the extended model it's really blue, green, red(Tokio), purple(async-std), and orange(smol) etc. I find that the sans-IO pattern is the best solution to this problem. Under this pattern you isolate all blue code and use inversion of control for I/O and time. This way you end up with the core protocol logic being unaware of IO and it becomes simple to wrap it in various forms of IO. 0: https://hugotunius.se/2024/03/08/on-async-rust.html https://hugotunius.se/2024/03/08/on-async-rust.html
- fsociety 3y agoThis is a cool pattern, thanks for the share.
- K0nserv 3y agoNo problem, here are some examples of it: * Quinn(A QUIC implementation, in particular `quinn-proto` is sans-IO, whereas the outer crate, `quinn`, is Tokio-based)[0) * str0m(A WebRTC implementation that I work on, it's an alternative to `webrtc-rs`. We don't have any IO-aware wrappers, the user is expected to provide that themselves atm)[1] 0: https://github.com/quinn-rs/quinn https://github.com/quinn-rs/quinn 1: https://github.com/algesten/str0m/ https://github.com/algesten/str0m/
- KiloCNC 3y agoI love the fact that people outside the Python ecosystem are spreading the word about sans-IO. I think it should be the next iteration in coding using futures-based concurrency. I only wish it were more popular in the Python land as well.
- zozbot234 3y agoThe Haskell folks got there first.
- Dessesaf 3y agoIs that sans-IO pattern doing anything particularly novel, or is that basically just a subset of what any functional programmer does anyway? Don't get me wrong, I'm fully on board with isolating IO. But why not go the slight extra step and just make it completely pure? You've already done the hard part of purity. The rest is easy. Then you get all those nice benefits of being generic over async and sync, but can also memoize and parallelize freely, and all the other benefits of purity.
- K0nserv 3y agoIt's been a while since my Haskell days, but I think the key difference is whether you are abstracting over the IO or if the IO sits outside the pure code entirely When you abstract over IO you have blue(pure code) that contains generic red, green, purple, or orange code. With sans-IO you inverted this so the non-blue code is driving things forward by calling into blue code. Rust, in particular, does not support abstracting over syncness at the moment, although there's work happening there. Even if you add support for that you also then need to abstract over the executor in use. My fear is that this will be too leaky to be useful, but we'll see. For now sans-IO is the best option in Rust.
- Kinrany 3y ago> Since nearly everything you might want to do in Rust probably involves IO with very few exceptions that means for the most part you should probably ignore non-async libraries regardless of whether the rest of your application wants it to be async or not. Only if you have two libraries to choose from and they are otherwise identical, which is rare. Using blocking code in async applications is not as seamless as it should be but not hard. Instead of writing `foo()` you write `tokio::spawn_blocking(foo).await`. It will run the new code in a separate thread and return a future that will resolve once that thread is done.
- Kinrany 3y agoAnd of course most libraries don't even need IO because the application can do it for them, so it only makes sense for them to be async if they're computationally heavy enough to cause problems for the runtime.
- zaphar 3y agoThat assumes you are using Tokio. As another poster said not only does the ecosystem fragment along the async/non async lines but along the runtime lines. Async is an extremely leaky abstraction. You are in a way making my point for me. If you want to avoid painful refactoring you should basically always start out tokio-async and shim in non async code as needed because going the other way is going to hurt.
- Kinrany 3y ago`spawn_blocking` is a single function and it is not complicated, it shouldn't be too hard for other runtimes to do the same. The end application does have to choose a runtime anyway and will have to stick with it because this area isn't standardized yet. This problem mostly affects the part of the ecosystem that wants to put complicated concurrency logic into libraries.
- cryptonector 3y ago`spawn_blocking` should be part of a core executor interface that all executors must provide.