6 ms·
Not to your main point, but I don’t think it’s fair to say Rust “botched” its async impl. There are certain features it’s missing, but nothing about the design
by skavi 2y ago
Not to your main point, but I don’t think it’s fair to say Rust “botched” its async impl.
There are certain features it’s missing, but nothing about the design really prevents them from being added. And many have been in the past few years.
I’d point to the “leakpocalypse” as a more fundamental Rust flaw.
- adastra22 2y agoHaving perpetually colored functions is a huge, fundamental annoyance with the Rust concurrency design.
- nine_k 2y agoWhat would be a good way to not have sync / async colors? Explicit yield / resume, like Lua? Full-on CPS?
- adastra22 2y agoGo handles it quite nicely. It's not perfect, but it is nice. Every function is a coroutine. There is no such thing as a non-async fn in Go. But if you want explicit await-like concurrency, then you can explicitly wait on a message from a channel. Every time I bring this up there are people who jump in with excuses for why Rust can't do something similar. But these are either (1) historical accidents in the sense of implementing goroutines would've broken existing code [fair], or (2) excuses. Go's approach is much harder to get right, and much more pervasive in the impact on the language. But when it's done well, it's really smooth to use. I use Rust as my daily driver because I want the safety guarantees that Rust gives, and cargo is pretty nice. But every time I need to do something highly concurrent, I wish I was working in Go...
- gulbanana 2y agoThere is a good reason why Rust can't do it the Go way - Go's always-async concurrency requires a fairly heavy runtime. Rust is designed to be able to support systems that have no runtime at all, or a specialised one. (https://embassy.dev/ https://embassy.dev/ is an example of a specialised async runtime for running async Rust on microcontrollers).
- adastra22 2y agoWhy wouldn't you be able to swap out the concurrency runtime for something else? Or not switch it off entirely when not needed (making the concurrency keyboards into syntax errors)?
- zarzavat 2y agoYou can, you can switch it off by declaring a normal non-async function. It's almost as if there's two fundamentally different types of functions that people might want to declare, necessitating two function colors. Go is a language that is primarily used for async programming, so the design of having everything async makes sense. Rust is a language that is primarily used for sync programming, async programming is catered for but it's not the focus of the language. So having everything be async doesn't make any sense at all.
- Dylan16807 2y ago> It's almost as if there's two fundamentally different types of functions that people might want to declare, necessitating two function colors. Since any function can get indefinitely stuck in a system call, the main difference between "sync" and "async" isn't whether you can pause execution, it's who can pause execution. And the main downside to the more flexible option is that it requires extra overhead in how the stack gets allocated. But why should the flexible/inflexible choice be made on a per-function basis? I feel like I'm manually annotating which functions will inline, except worse because certain combinations won't compile. Instead, how about having the compiler figure it out on a per-call basis?
- kamov 2y agoRust used to have a GC in the past, as well as green threads and a bigger runtime. All of this has been explicitly removed before Rust 1.0. The reasons are well documented in many pull requests and RFCs for the language. The way the async feature works in Rust is that the asynchronous function is just a syntax sugar that gets desugared into a state machine struct during compilation. The way this state machine works is similar to how one could achieve async in a language like C. It's unfair to dismiss everything as excuses given that the fundamental aim of the language is different. In Rust async functions are not really colored because again - the async function is just a syntax sugar for a struct you can create and use in a sync context. The colors analogy is only really applicable in a language like JavaScript, where there's no way to poll an async function in a sync context.
- adastra22 2y agoHere's the thing though: Rust could have every function be an async state machine, automatically. And then the compiler optimizes away that code when it isn't needed. It would be a big pain to implement, but it's doable, and it would deliver a developer experience much closer to Go's. There isn't a technical reason for why Rust couldn't do this. FYI you can't poll an async result in a sync context in Rust, either.
- dwattttt 2y ago> FYI you can't poll an async result in a sync context in Rust, either. Sure you can. Async runtimes in Rust are written in Rust, that's exactly what they do.
- Ygg2 2y ago> There isn't a technical reason for why Rust couldn't do this. First, it would be a huge undertaking. That in itself is a huge time/resource burden. Second, it would add overhead to any non-async function call. Because async introduces branching on every function invocation, it would make the resulting assembly even harder to understand. This strongly goes against the zero overhead/ zero cost abstraction idea of Rust. By the same measure, Go could technically remove (almost) all GC, add some kind of borrowing mechanism and steal Rust's thunder.
- elcritch 2y agoElixir/Erlang, Go, Java all do. Java is new to the game with “green threads”.
- eikenberry 2y agoIf you decide to go with the async/await model, why not just go all in and have every function be async/await always? Gets you concurrency support without Colored functions. Is there a language like this?
- adastra22 2y agoGo.
- skavi 2y agoI haven’t used Go, but I am fairly certain it’s not really trying to do the same things as Rust. IIRC, calling C from Go isn’t zero cost. It also includes a much larger runtime than Rust.
- adastra22 2y agoYes, calling C from Go has significant overhead for exactly this reason. It basically requires creating a stack and copying over all the current coroutine info into it. Depending on the application, this can be accelerated by maintaining a cache of stack objects to use, or by having a "noasync" mode of compilation with all the coroutine stuff turned off (making calling C zero cost again), or by having a producer-consumer channel to a noasync thread that gets stuff passed to it from coroutine land and calls out to C. Generally speaking, applications tend to either (1) exist as low-level shims or embedded code that doesn't use coroutines, (2) be large Rust projects that don't call out to non-Rust stuff often (e.g. a web server), or (3) large performance-sensitive applications that call low-level APIs frequently (e.g. games). The above solutions handle all three of these cases. What Rust chose instead was to optimize for a compilation mode which supports all three cases at once, with zero cost for FFI. This sounds nice, until you realize that what they traded off to get this was UX. Writing concurrency in Rust is lightyears better than C++, but it still sucks compared with Go or Erlang.
- skavi 2y agoThat sounds like a few different languages. And I think it’s fine to want a language like Rust that allows you to be less explicit about things. Based on what I’ve heard, you might just be looking for Gleam. But I don’t think it would be the right move to make Rust something that it’s not, just so people don’t have to think about what a Future is or isn’t.
- skavi 2y agoHow so? It’s trivial to block on a Future or to call a function within a Future. I could imagine another language where all “functions” are Futures, but I don’t think that would be the right move for Rust. At the very least, that alternative language would probably have to provide a default executor. IMO, that’s a bit much for a language that aims to have a very minimal runtime.
- conradev 2y agoDoing that in Rust can be a footgun if the synchronous code blocks for too long. You often want a separate threadpool for calling those functions. It works great in Swift, though! Swift mitigates a lot of the function coloring pain: synchronous methods can conform to asynchronous protocol requirements, thanks to the compiler generating thunks (at least, I think that’s how it works)
- adastra22 2y agoA thunk wouldn't be zero-cost.
- skavi 2y agoOf course. But the common case of calling a synchronous function from a Future is something trivial like a + b.
- metaltyphoon 2y agoIsn’t this what spawn_blocking and block_in_place do?
- saagarjha 2y agoThe opposite, actually. Swift (Concurrency) doesn't really have a good way to do the kind of thing spawn_blocking does. (Though, some would argue that the very design of spawn_blocking is not good to begin with.)
- conradev 2y agoYes! In Swift there is no need to do this sort of differentiation, though, which is more ergonomic. Different trade-offs.
- saagarjha 2y agoSwift has the same problem, FWIW.
- danpalmer 2y agoThat may be more fair. I guess a better way of putting it is that the async/await implementation was fraught with drama and that it did not really succeed in getting the whole community onboard and pulling in the same direction. Python, for example, continues to have similar issues, whereas Node/JS definitely did stick the landing, and Swift arguably did too.
- skavi 2y agoYup, that’s an entirely fair characterization. One I probably should have understood from your initial comment :).