Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

This is mostly about async Rust. Today, async Rust works, but it's a lot jankier and less polished than the rest of the language; in some ways, it feels kind of beta-quality. The maintainers are extremely aware of this problem and are making serious efforts to fix it. These efforts are expected to take years, because it is a difficult technical problem (most other languages solve it in ways that compromise runtime performance or low-level programmer control, which Rust prioritizes) and because a community-driven open source project has limited resources and takes time to arrive at decisions.

This blog post discusses one potential technical direction for addressing a subset of the problems with async Rust, and in particular why the need to preserve backwards compatibility with earlier design decisions, made before async Rust was designed, makes it harder than it might otherwise have been.

If you don't use async Rust, you can mostly ignore this. If you do use async Rust, and are interested in how it might (backwards-compatibly!) evolve in the coming years, then the report of the Async Foundations Working Group is probably the best starting point to understand what's going on: https://rust-lang.github.io/wg-async/welcome.html



With respect to Rust, a big part of the issue here is that Rust is roughly attempting to turn Javascript into a systems programming language. While `async` as a language feature is a key part of the ideas of JS, it's essentially fundamentally incompatible with 0-overhead systems programming (despite most systems programming using asynchronous concepts).

Asynchronous systems code is usually done completely stackless, using state machines and queues of objects, and batched functions that run to completion to transition objects to the next state. This gives you maximum cache-friendliness, and since good performance fundamentally comes from limiting your options, making unbounded state growth hard is a pretty good way to get it. Async functions with stacks are already a performance compromise in comparison. You can do this type of programming in Rust by just not using async.

Unfortunately, some fraction of Rust maintainers are pulling back towards Rust being a web backend language (see the earlier comment about Javascript), and that really needs a concept of stackful async, but really doesn't need the same level of performance as a kernel or a filesystem. Rust green threads would even have worked really well for this use case, despite being a huge mismatch with traditional systems programming. In addition, those async functions need some sort of runtime anyway, so it's not even really a standalone language feature distinct from the packages that enable it.

So here we are, in a situation around async that makes nobody happy. C++ coroutines, despite the hate they get, and Go green threads, despite the performance, actually seem to be closer to the mark.


> So here we are, in a situation around async that makes nobody happy. C++ coroutines, despite the hate they get, and Go green threads, despite the performance, actually seem to be closer to the mark.

The story that gets told -- and for some reason, I tend to believe it, even though I'm much less idealistic than 10 years ago -- is that folks in the Rust community (and not just them of course, also C++ people and lots of others) tend to really push hard against abstractions that aren't both ergonomic and zero-cost. It is frankly kind of amazing to see the solutions that emerge after months and years of what somewhere resembles either head-butting or crazy experimentation. The path to getting there is messy, and it seems like maybe that's where we are now.


Rust async is stackless. While it's not as seamless as javascript yet, it too started with generators and janky promise libraries before - a decade later - support for it became ubiquitous.

Rust will undergo a similar journey where the compiler will continually make improvements to ease of use, arriving at a very elegant solution that's just as easy to use.


I'm pretty sure Rust people invented a new meaning for the word "stackless": in Rust's context it means "does not depend on an underlying architectural stack." It does not mean "does not use the concept of a stack." That would be why you can have stack traces of async functions in Rust.

In practice it means allocating a stack for your function of the maximum size you would need.


Rust didn't invent the terminology.

Rust has stackless coroutines. Go has stackful coroutines. The literal distinction is that stackless coroutines exist independently of a stack: you have a state machine object that, when it's resumed into, is put onto the stack and calls normal functions through the run-time stack. Stackful coroutines are essentially lightweight threads (hence "green threads" or "virtual threads"): the coroutine comes with a miniature run-time stack.

In practice, stackless coroutines are more space efficient and have lower context-switch costs. A big factor for Rust specifically is interoperating with C. Stackful coroutines make writing "straight-line" code simpler (no function coloring) but introduce more overhead.


async is just a syntactic sugar to transform a function into the state machine you would have otherwise written. If you don't trust the compiler to perform this transform adequately, you can write a Future by hand. There are users using async/await syntax to write software for microcontrollers without dynamic memory allocation, runtimes like tokio are not a hard dependency of async/await syntax or the Future abstraction.

C++ coroutines perform the same transform, but they dynamically allocate that state machine for every coroutine (unless the optimizer can eliminate the allocation). In contrast, Rust inlines every coroutine into a single state machine until you explicitly tell it to allocate it. I don't know why C++ chose a less "zero cost" solution; I suspect the fact that they don't have memory safety and so letting you own a coroutine's state is very footgunny in C++ played a part in their choice.


Can you explain what makes C++ coroutines work better than Rust futures?




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

Search: