Rust made the deliberate decision to avoid the heavier Go goroutines runtime model after early alpha/beta experiments showed it conflicted with Rust's low-level design. I found 3 links to some history of that rationale in a previous comment:
https://news.ycombinator.com/item?id=28660089
And some more links:
https://stackoverflow.com/questions/29428318/why-did-rust-re...
https://github.com/rust-lang/rfcs/blob/master/text/0230-remo...
And lots of debate in this previous thread: https://news.ycombinator.com/item?id=10225903
But this practically requires some kind of garbage collection and a fat runtime.
I think it was a good decision on the Rust team to abandon this and go for a low level systems programming language. Otherwise it would've been just another Go-like language that isn't really usable in low level systems programming.
Implementing portable async language features without a fat runtime or garbage collection is novel work so it's no wonder that it's taking its own sweet time to reach maturity.
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.
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.
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)
?
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.How does this differ from Rust (Or Typescript etc.) where we would use
async f() -> i32 { }
fn g() { f().wait() }
to block?A better comparison would be between the Rust and C++ paths to async - C++ also spent years designing their async system, and the end result is divisive at best.
Not saying that is the ultimate answer to building async and/or parallel algorithms, but being aware of what others in the same space are doing is certainly useful.
Go’s runtime model just makes stuff like this vastly simpler. Rust can’t impose the same kind of runtime model that go has.
TinyGo is another Go compiler intended for embedded systems. And now its officially sponsored by Google.
Go and Java (with Loom) have these lovely facilities, but it is hard to interface with them if your language lacks these features. I find it odd that C#, javascript, and python don't provide the smoother async Go experience despite having runtimes / VMs.
Unfortunately, according to one of the lead developers, the system couldn't keep up with Mozilla's throughput and reliability requirements due to limitations of go's built-in features.[0] They announced they would re-write a new solution in c ("Hindsight") and they basically left an entire community of users high and dry due to not being able to salvage the go-based project, since it relied so heavily on the built-in features.
[0] https://heka.mozilla.narkive.com/9heQ11hz/state-and-future-o...
It’s not that Rust has struggled, it was never Rust’s priority to have a runtime or high level async code. It had very different goals to Go.
It’s like asking “why has C struggled to implement Promises like JavaScript”? The languages serve different purposes.
You’re right it wasn’t their initial focal point but later on Rust wanted to offer the chance of having a go-like runtime without destabilising the low-level performance at the core level. I.E only those that use it pay for it and those that don’t use it aren’t affected.
Offering “zero cost” futures etc is very difficult to do.
See https://aturon.github.io/blog/2016/08/11/futures/ for more info, old but still relevant (including the chart)
Like adding a GC for the async feature.
C++ ones can interoperate with any type that plugs into the compiler magic expected by the co-routines code rewrite.
If you want to deep dive into how Visual C++ does it, and how WinRT gets plugged into C++ co-routines, here is a very lengthy set of blog posts.
https://devblogs.microsoft.com/oldnewthing/20210504-01/?p=10...
Now back to Rust, how can I interoperate across tokio, async-std, smol and fuchsia-std, as easy as I can between WinRT, and others?
Not sure I understand, what kind of interoperability you are talking about. What kinds of code works in C++ across runtimes, for which the equivalent in Rust doesn't?
Or has that situation been sorted out by now?
In Rust, you don't need to use a specific runtime if you just want to use async function in your library.
For example, Go runtime imposes unavoidable overhead in memory usage, because each goroutine must have its own allocated stack (Rust futures, on the other hand, are stackless). Rust runs on low memory platforms where Go isn't really suitable.
Architecture of an efficient async runtime is going to be different for 128-core server vs single-threaded chip with barely any RAM. In Rust you can write your own runtime to your needs, rather than fight overhead of a big runtime on a small device, or struggle to scale a dumb runtime to complex workloads.
For instance, C is a great abstraction. You take assembly language, abstract away manual management of registers with variables and pointers, add structured types to describe memory layout, standardize flow of control operations, and add functions to enable code reusability, and you have something which is very easy to work with and also to understand. It's not 100% on par with assembly in terms of performance, but it's pretty darned close, and with a little bit of practice it's very easy to look at a block of C code and basically understand what equivalent assembly it compiles to. It's a great abstraction, and it's no wonder that a vast majority of the languages which have come after it have borrowed most of its major features.
I would argue we haven't really had a "great abstraction" to the same level since then*. There have been efforts to abstract away memory management the way register management has been abstracted away, and many of them have been successful for a lot of use-cases, but not to the point that everyone can forget about memory management the way the vast majority of us can forget about register management. Garbage collectors can be too slow or too wasteful for a lot of use-cases, and you need essentially another program you didn't write to pull it off. In a GC'd language it's not so trivial to look at a block of high-level code and predict what your CPU will do. There are other approaches: like the structured approaches of Rust and Swift which are quite interesting, but they're far from proven at this point.
Similarly I think we're not quite there yet with concurrent programming. As far as the transparency topic, a lot of async implementations are more in the direction of garbage collectors, where the compiler rips apart your code and builds a state machine in its place. It's not hard to believe that the result will be difficult to work with and reason about in some cases.
And maybe the problem is that most approaches to async are trying to cram concurrent execution into that C-like abstraction, which is such an elegant abstraction for single-threaded execution exactly. Maybe concurrent programming needs to be re-thought from first principals, with different primitives involved.
*Aside: if there is another "great abstraction" on the horizon, I believe it to be ADT's. That is a feature of programming which feels like a clear step forward with no clear downsides. It's a shame that they haven't been included in Zig.
The one drawback I see is that it seems a bit verbose: i.e. the tag set itself has to be declared as a separate enum, and then the tags need to be repeated inside the union.
So it looks to be slightly bolted-on and unergonomic (similar to TypeScript's implementation) but I haven't worked with it so maybe I am missing something.