Hacker News new | past | comments | ask | show | jobs | submit login

Yes, but having coded in both languages you can see how Go's concurrency system works well with it and fits neatly.

Try using async in Rust and you'll see what I mean, it sucks to use it.

The same applies to cpp, you'll need years and years of experience to write somewhat decent cpp what you can write in Go with a few weeks learning coroutines, and will still be prone to make mistakes. I've done lots of cpp and can affirm that after decades I'm still not sure if my concurrent solution will run as well as my half-assed Go coroutine code.

Go coroutines is a good abstraction that is very fitting to the language, similar to the actor model and the BEAM VM (erlang/elixir).

Use the same abstraction in another language (like JVM or C) and you'll see how the devex makes a huge difference.




I'd chose async/await over threads any day, and many people do too, that's why this construct has gained so much popularity in many different languages. Async/await is a rare example in PL design where a new concept is implemented in one language and then adopted by many in the following years.

In fact, the “threads are more ergonomic than async/await” seems to be more of an HN meme than a popular opinion in the wild. (And please don't quote What color is your function since it's mostly a rant about callback-based concurrency which everyone agree it sucks).


> 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…


I'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.


Rust 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.


I 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.


I think you mean block_on. Spawn blocking would be for your first use case, calling sync from async.


Yes, thank you, my bad. Fixed.


> 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…


> 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.


But even Go have a red/blue distinction (functions that return an error and the ones that don't), and in practice it's so fine it never occurred to you that it was in fact a red/blue split!

The real problem came in JavaScript with the callback hell, everything else is a minor issue that nobody really notice.


Not "threads are more ergonomic than async/await", but "threads and channels can be ergonomic".

I've been a big fan and user of async/await in various languages for over a decade now, and it can absolutely improve reading sequential asynchronous logic flow, but lets not pretend it isn't without tradeoffs. Compared to wrangling synchronization contexts and function colors, channels can be rather straightforward.


Errors as return value are a “function color” too (in practice, you need to propagate it upwards the stack and it “contaminates” your code the same way as async/await). And in fact it interacts better with async/await (like in Rust) than with channels…




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: