Hacker News new | ask | show | jobs
by stubish 1658 days ago
The saddest part of learning Rust was discovering that there are no goroutines and that async works like Python and everything needs to be written twice to support both async and blocking styles. Like it was 20 years ago all over again and I'm still trying to mix Twisted and stdlib Python. I got all excited thinking of how the borrow checker would work so well with coroutines, only to discover it got nobbled because Rust's use cases include embedded systems and no runtime (like in a golang 9MB hello_world.exe). I have no idea if Rust could evolve its concurrent programming support to something better than Go's, even if it did drop some of its shackles.
2 comments

I find traditional multi threading a pleasure in Rust, and it works really well with the borrow checker and how the type system is designed (like the Send and Sync traits).

On the semantic side, extending it to a N:M threading model like Erlang or Go would work great. But that model only seems to work well if you basically make the entire language async, which is in conflict with too many of rust's goals. So we are left with the somewhat awkward state of async as a second class citizen.

I only have rudimentary knowledge of Golang (but think the blocked/green automatic scheduling is excellent).

How does go nest aync calls?

func f() { }

func g() { }

func h() { go g() go f() }

What happens on

f()

Are the g() and f() calls inside h() blocking? Or are they async and the block happens at the point of return? Which would be the main difference to languages with an async keyword, were you need to be explicit about blocking.

The go keyword executes the called function asynchronously so g() and f() won't block h(). If you need a computed result from g() or f() then you'll need to use a channel or a shared mutex guarded value to get it. A channel is the correct default choice and the mutex should only be used if you need it for performance or other reasons.
I understood from the OP that in Golang the sync and async code would be the same - contrary to e.g. Rust were you have async/wait. Go achieves this with coroutines and the go keyword.

f()

is a sync call to the function f, and the function f used async calls inside. Somewhere then needs to be a transition from async to sync contexts (aka wait/block).

I wondered where this happens.

From your comment I assume there is a difference, in sync code I would do

x = f()

while in async code I would use

f(channel)

?

Close. If I execute say want to run some function asynchronously I use the go keyword to execute it. But I can't get a return value if I do that so I need some other mechanism to get the return value. One way is to pass a channel into the function and expect the function to return the value to me via that channel. like so:

    func f(ch chan[int]) {
     ch <- 1 // depending on the channel implementation this is a blocking action
    }
then I can call that function asynchronously

    go f(ch)
and later when I want the value from f I can retrieve it from the channel

    i := <-ch // this is a blocking call
The net effect of the all the above is that async and non async code is highly composable. if I have a function that computes a value and I want to get that value asynchronously then I can wrap it in a function that uses a channel to get the value to me.

    go func() { ch<-f() }
Every function is a potential asynchronous function.
"The net effect of the all the above is that async and non async code is highly composable."

How does this differ from Rust (Or Typescript etc.) where we would use

  async f() -> i32 { }

  fn g() { f().wait() }
to block?
Thanks a lot! Can only upvote you once sadly.