The Async Rust working group has been revisiting our roadmap for the next few years. We'd like to share our plan for what we'll accomplish in 2023, 2024, and beyond.
First, we have sharpened the scope of the roadmap to only include language and standard library features.
Tooling and compiler-based improvements are still part of our overall vision for async, but their dependency graphs look very different.
We're more confident in our ability to drive this narrower scope of features and deliver them on a given timeline. We think it would also be great to have others step up and drive the other areas.
These are the criteria and beliefs that we used to form this roadmap. They are ordered such that, if in conflict, we give priority to the first one.
Steady progress. People are building foundational layers in async Rust now. We need to ship incrementally, building our way to the end goal, but improving stable Rust along the way. This means maintaining a razor-sharp focus, even as we consider how later developments might interact with our current goals.
Stay ambitious. We need to continue supporting the async Rust code that exists today, but we shouldn't be held back by it. The current experience isn't where we want it to be, and we shouldn't be afraid to propose and make ambitious changes, as long as there's a path from here to there.
Enable the ecosystem. The primary role of the Rust org is to build the interconnections that allow the ecosystem to scale and interoperate. Sometimes standardizing one layer simplifies further development atop it, allowing for a flourishing ecosystem.
Drive consistency. The async Rust experience right now varies dramatically depending on your domain, and getting started involves a lot of decisions with non-obvious and long-lasting implications. We need to find ways to make async Rust more uniform.
See also the Rustacean design principles (Reliability, Performant, Supportive, Productive, Transparent, Versatile, in that order). The Async Rust experience as it stands now arguably puts Versatility above Reliability, Supportiveness, and Productivity through constructs like select!
and FuturesUnordered
as well as the lack of portable libraries and idioms that work uniformly across runtimes.
Also available in table form.
Generally speaking, each item in this diagram depends on the items beneath it. Solid lines are for things we know we want, dashed lines represent stepping stones, and dotted lines are for things we aren't sure about yet.
This year we will stabilize the MVP for async fn in trait. This includes:
async fn
in trait-> impl Trait
in traitSend
boundsWhatever combination of features we end up with, they must be sufficient for 3 or more representative use cases to publish stable APIs with. Some less-ergonomic workarounds may be required, as long as fixing them won't require breaking APIs later.
The MVP does not include dynamic dispatch or "complex" cases of Send bounds.
In order to ship this year, we'll need a stabilization PR to land sometime in early Q3. This means we need to be firming up open questions and driving to consensus now.
By the end of 2024, async will have an equal standing in Rust's most commonly used abstraction mechanisms. This should make async feel like an integrated part of the language, supercharging productivity.
First we want to stabilize an async Iterator
trait, including combinators like map
. This involves solving real-world problems with async, captures, and generic code, particularly async closures.
Next, round out the async fn
and RPITIT stories by making dynamic dispatch work using dyn Trait
. This should remove all use cases for the async-trait
crate.
Finally, make it possible to publish portable implementations of protocols using official traits for runtime interop and basic I/O. Representative use cases are hyper
and quinn
.
2024 will be packed, with a lot of exciting possibilties opening up for the ecosystem. If everything goes according to plan this year, we should have most of the second half of 2023 to work on these features, in addition to the first half of 2024.
With many of the ecosystem's most fundamental limitations out of the way, we'd like to shift our focus making async Rust more reliable and supportive.
We expect reliability improvements to come mainly through new patterns for managing concurrency ("Combinators 2.0" and structured concurrency), along with a mechanism for async destruction. The fundamental work on abstraction mechanisms like async closures will enable this. It will also be a key thing to look forward to as we work on a Spawn
trait in 2024.
We will support async Rust users by making it much easier to get started with a notion of a global executor for use with async fn main
, a built-in default runtime, and async APIs for files and sockets in the standard library. We will continue making it possible to customize your runtime while using these features.
Finally, we will deliver further productivity improvements through await patterns (for await
). We're also interested in [async] generators and keyword generics, but tend to see those as separate initiatives.
These features are far enough in the future that many details may change. We expect them to come more into focus over the next year or two. Around this time next year (end of Q1 2024) we will add a 2025 milestone and pull some of these features into it.
After shipping fundamental abstraction features like async closures and dyn Trait, we can shift our focus directly to the reliability question and start publishing our new opinionated patterns for use in the broader ecosystem. That would mean de-prioritizing productivity and interop.
In terms of the tenets, this would place ambition and consistency over continuous progress and enabling the ecosystem.
It's true that reliability is one of the most significant problems async Rust faces today, compared to synchronous Rust, and it should be given high priority. It's also likely to be one of the tougher problems to crack, given the diversity of use cases and the many challenges of language design involved. And given its dependence on async closures, there might not be enough iteration time to ship both of these in the same year.
On the other hand, we think we have a pretty good handle on what it would take to ship basic I/O traits. Spawn and timer traits require taking an opinionated stance, but only in a pretty narrow sense of whether we can express a structured concurrency paradigm using them, and that doesn't seem too hard to accommodate. Finally, async Iterator
is a clear goal that requires us to exercise our generic programming muscles and may also itself be useful in building them.
There's a chance some of this experimentaion will happen anyway in the meantime. We should do what we can to encourage experimentation with crates like futures_concurrency
. One of the ways we'll do that, of course, is by shipping core abstraction mechanisms. We should then revisit this question when planning for 2025.
There's been some recent debate over whether async fn next
is the right core primitive for async iteration, rather than poll_next
. It would be easier to write an async next
implementation, but the fact that async functions must return a separate object leads to irregularities in how the async version of the trait would work:
async Iterator
and the future returned by next()
in the same struct, because one borrows from the other.dyn async Iterator
always requires boxing or an equivalent adapter.If we did adopt poll_next
we would almost certainly want to prioritize generators and async generators, so these could easily be written. This does have a lot of benefits and reduces the need for combinators.
Alternatively, we could come up an easy way to still write a next()
-like implementation as an async function. But it's debated whether this would actually make user's lives much easier, since you are still having to write a state machine by hand.
If we did prioritize async generators, we would have to pick something to deprioritize. Maybe portable protocols, since that's less "fundamental" and we want to keep dyn
to finally finish AFIT. Or maybe we bump dyn
, because it becomes less important in a world where async Iterator
is already dyn-safe.
source: Github project