11 ms·
> since it's mostly a rant about callback-based concurrency which everyone agree it sucks It really isn’t, it’s a rant about there being two worlds (colors) of
by cube2222 2y ago
> since it's mostly a rant about callback-based concurrency which everyone agree it sucks
It really isn’t, it’s a rant about there being two worlds (colors) of functions - async and not async, which don’t compose that well together. I believe Rust is exploring the ability to genericize functions over their asyncness, and I wish them luck in that endeavor.
Async/await is, if anything, a tradeoff for performance and ffi over usability. As they are generally stackless and in the case of rust don’t need allocations.
With green threads like in Go you’ll generally have to allocate at least a small stack for each goroutine, and you’ll have to fix up things for ffi, but it’s much more straightforward, all functions have the same color (you just write “blocking” code, but the underlying implementation is async), you don’t have an ecosystem split, and you don’t have to war with the type system every other day (as many feel like they’re doing in Rust when writing async).
It’s a pretty good point in case that Java just spent a few years making project loom happen, which effectively brings the Go approach to Java.
Async/await has its place, especially in very performance-critical code, but its usability is, IMO, miles behind threads.
Of course, an ideal world would be green threads with rusts compile-time checking of concurrency safety…
- dgroshev 2y agoI'm not sure what you mean by "don't compose well". You can call sync code from an async function perfectly well. Going the other way around is either very simple in a throwaway code (just use pollster or tokio's block_on), or requires appropriate care (because async means IO and calling a function doing IO means your function now does IO too). I believe what most people are struggling with is having to think about doing IO instead of just doing IO wherever. But that's a feature, not a bug, and an overarching theme in Rust: you need to think what you're doing and how everything will work together. Yes you can't just put an async call somewhere down the stack without changing the signatures, but that's a good thing.
- LtdJorge 2y agoI think you mean block_on. Spawn blocking would be for your first use case, calling sync from async.
- dgroshev 2y agoYes, thank you, my bad. Fixed.
- cube2222 2y agoRust doesn’t model IO in terms of types though, does it? Sure, an async function likely does IO, but maybe it’s just receiving from a channel? At the same time 90% (completely made up number) of Rust just does plain blocking IO and you absolutely won’t get that in the signature either (other than looking into error types, that is). We’re not in Haskell here. You can just make a blocking call in your async code, not notice it does IO (or you’re just not proficient in async), and it falls to pieces, your runtime just hung. While “you can just use pollster” means you now have to wrap every function call to an async-first library in your async-less codebase. Again, this isn’t “composing well” in my book, it absolutely does create a divide in the ecosystem. While in Go all code is written in a blocking fashion, but all IO is done async (either via threadpool or non-blocking syscalls), handled by the runtime, because the threads are green. Of course, that’s less performant and falls apart if you start using C libraries that do blocking IO (but it’s not like Rust would do better in that latter case). At the same time, whether modeling IO in the type system is good or not, is I think something where opinions very much differ. To be clear, again, I’m not bashing Rust async in general, I just think it’s a tradeoff and I very much think its usability is worse than just writing straightforward blocking code.
- dgroshev 2y agoI disagree with your "just" in "just receiving from a channel". Receiving from a channel is hardly distinguishable from IO, since it can take an arbitrary amount of time and the receiver needs to handle external resource failures (ie the other side of the channel getting dropped). That's very different from normal sync functions parsing JSON, adding matrices, or doing date math. Also note how functions that can't block are still sync, like tokio::sync::oneshot::Sender::send. I also can't agree with the argument that if it's possible to hang the runtime there's no point in explicit async. Yes people can still make mistakes, but it's better when it's explicitly a mistake and not just haphazard IO everywhere being the norm. What is the "async-less" codebase for? Why does it have to be "async-less"? A Go codebase is fully async at all times, so why shouldn't a Rust system extend async up to main() (or at least encapsulate IO-heavy parts in a separate async part)? Runtime behaviour of async rust is ~equivalent to Go, so coming back to my original point, the difference is that 1) Rust allows to have non-async, predictable functions, async is opt-in 2) the opt-in must be explicit 3) you have more choice between intra- and inter-future concurrency (select! vs spawn). All those points are important and empowering, they aren't problems to be solved. I agree that the flip side is that Rust forces explicit decisions upfront, but that's one of the core premises of Rust.
- littlestymaar 2y ago> It really isn’t, it’s a rant about there being two worlds (colors) of functions - async and not async, which don’t compose that well together. Except the composition problem only manifests itself badly when your “red function” is callback-based. Re-read the blog post and you'll see that the fundamental problem is that red functions are unwieldy (which it really is when you have callbacks, but not when you have async/await). In Rust async/await is exactly as contaminating as `Option` or `Result`, or “error as return values” in Go or C, which creates the same split (functions which can propagate an error and the ones which don't), but it's almost never being discussed in terms of “function color” because it's not in fact such a big deal. > I believe Rust is exploring the ability to genericize functions over their asyncness In fact they (well, in practice mostly Yoshua) would like to genericize over all effects including fallibility because they are well aware that the problems are equivalent. The biggest problem in Rust's case being the combinatory explosion between different effects. > Async/await has its place, especially in very performance-critical code, but its usability is, IMO, miles behind threads. > Of course, an ideal world would be green threads with rusts compile-time checking of concurrency safety… This is entirely subjective: as I said above I would never willingly works with threads if I can use async/await instead. It's not just a performance thing at all. For many people async/await is just a superior user experience. In fact it's exactly the same as exceptions vs `Result`: (unchecked) exceptions don't make colored functions whereas `Result` does, but exceptions, like threads hide where something can fail on your code (resp. when your code can get stuck waiting for IO) and this explicitness has value even if it comes with a cost in terms on annotation effort. Telling someone that prefers async/await that thread is absolutely better is exactly like telling someone that dynamic typing is better than static typing because you have less things to when writing code…
- afdbcreid 2y ago> Except the composition problem only manifests itself badly when your “red function” is callback-based. Re-read the blog post and you'll see that the fundamental problem is that red functions are unwieldy (which it really is when you have callbacks, but not when you have async/await). Not only; the problem also manifests when you interoperate with blue functions, for example because they are provided to you by libraries.