News for hackers. Stuff that matters.
Re:Why choose async/await over threads? by zaphar on Monday March 25, 2024 @08:55AM (#39815707)
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.

Re:Why choose async/await over threads? by K0nserv on Monday March 25, 2024 @09:20AM (#39815944)
I 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

Re:Why choose async/await over threads? by KiloCNC on Monday March 25, 2024 @12:14PM (#39818138)
I 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.
Re:Why choose async/await over threads? by zozbot234 on Monday March 25, 2024 @12:53PM (#39818590)
The Haskell folks got there first.
Re:Why choose async/await over threads? by fsociety on Monday March 25, 2024 @10:09AM (#39816532)
This is a cool pattern, thanks for the share.
Re:Why choose async/await over threads? by K0nserv on Monday March 25, 2024 @10:13AM (#39816600)
No 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

1: https://github.com/algesten/str0m/

Re:Why choose async/await over threads? by Dessesaf on Wednesday March 27, 2024 @07:56AM (#39837885)
Is 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.

Re:Why choose async/await over threads? by K0nserv on Wednesday March 27, 2024 @02:32PM (#39842894)
It'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.

Re:Why choose async/await over threads? by Kinrany on Monday March 25, 2024 @10:08AM (#39816513)
> 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.

Re:Why choose async/await over threads? by zaphar on Monday March 25, 2024 @10:22AM (#39816707)
That 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.
Re:Why choose async/await over threads? by Kinrany on Monday March 25, 2024 @10:37AM (#39816879)
`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.

Re:Why choose async/await over threads? by cryptonector on Monday March 25, 2024 @11:22AM (#39817454)
`spawn_blocking` should be part of a core executor interface that all executors must provide.
Re:Why choose async/await over threads? by Kinrany on Monday March 25, 2024 @10:10AM (#39816553)
And 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.
Re:Why choose async/await over threads? by HideousKojima on Monday March 25, 2024 @09:04AM (#39815787)
>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.

Re:Why choose async/await over threads? by zaphar on Monday March 25, 2024 @10:08AM (#39816514)
Well, 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.