- Feature Name: (fill me in with a unique ident,
my_awesome_feature
) - Start Date: (fill me in with today's date, YYYY-MM-DD)
- RFC PR: rust-lang/rfcs#0000
- Rust Issue: rust-lang/rust#0000
- Goal owners: tmandry, nikomatsakis
- Goal teams: Lang, Libs-API, Libs
This is a proposed flagship goal for 2024h2 covering async Rust. You can read more about the project goal slate and its associated process here. This RFC is prepared using the project goal template, which differs from the typical RFC template.
The overall goal is bringing the Async Rust experience closer to parity with sync Rust. We have identified three high-priority goals that we believe would do the most to improve async over the long term:
- resolving the "send bound problem", thus enabling foundational, generic traits like Tower's
Service
trait; - stabilizing async closures, thus enabling richer, combinator APIs like sync Rust's
Iterator
; - reorganizing the async WG, so the project can benefit from a group of async rust experts with deep knowledge of the space that can align around a shared vision;
We have also identified two "stretch goals":
- stabilizing a trait in libstd for async iteration, thus enabling the ecosystem to build atop a stable foundation;
- completing the async drop experiments proposed in MCP 727, laying the groundwork for resolving the the next major gap in language feature support.
Approving this goal implies agreement from the Lang, Libs, and Libs-API teams to the items marked as in the table of work items, along with potentially other design meetings as needed.
In 2024 we plan to deliver several critical async Rust building block features, most notably support for async closures and Send
bounds. This is part of a multi-year program aiming to raise the experience of authoring "async Rust" to the same level of quality as "sync Rust". Async Rust is a crucial growth area, with 52% of the respondents in the 2023 Rust survey indicating that they use Rust to build server-side or backend applications.
Async Rust is the most common Rust application area according to our 2023 Rust survey. Rust is a great fit for networked systems, especially in the extremes:
- Rust scales up. Async Rust reduces cost for large dataplanes because a single server can serve high load without significantly increasing tail latency.
- Rust scales down. Async Rust can be run without requiring a garbage collector or even an operating system, making it a great fit for embedded systems.
- Rust is reliable. Networked services run 24/7, so Rust's "if it compiles, it works" mantra means fewer unexpected failures and, in turn, fewer pages in the middle of the night.
Despite async Rust's popularity, using async I/O makes Rust significantly harder to use. As one Rust user memorably put it, "Async Rust is Rust on hard mode." Several years back the async working group collected a number of "status quo" stories as part of authoring an async vision doc. These stories reveal a number of characteristic challenges:
- Common language features do not support async, meaning that users cannot write Rust code in the way they are accustomed to:
- Common async idioms have "sharp edges" that lead to unexpected failures, forcing users to manage cancellation safety, subtle deadlocks and other failure modes for buffered streams. See also tmandry's blog post on Making async Rust reliable).
- Using async today requires users to select a runtime which provides many of the core primitives. Selecting a runtime as a user can be stressful, as the decision once made is hard to reverse. Moreover, in an attempt to avoid "picking favories", the project has not endorsed a particular runtime, making it harder to write new user documentation. Libaries meanwhile cannot easily be made interoperable across runtimes and so are often written against the API of a particular runtime; even when libraries can be retargeted, it is difficult to do things like run their test suites to test compatibility. Mixing and matching libraries can cause surprising failures.
Based on the above analysis, the Rust org has been focused on driving async/sync language parity, especially in those areas that block the development of a rich ecosystem. The biggest progress took place in Dec 2023, when async fn in traits and return position impl trait in trait were stabilized. Other work includes documenting async usability challenges in the original async vision doc, stabilizing helpers like std::future::poll_fn
, and polishing and improving async error messages.
Progress on async-related issues within the Rust org has been slowed due to lack of coherence around a vision and clear steps. General purpose teams such as Lang and Libs-API have a hard time determining how to respond to, e.g., particular async stabilization requests, as they lack a means to judge whether any given decision is really the right step forward. Theoretically, the async working group could play this role, but it has not really been structured with this purpose in mind. For example, the criteria for membership is loose and the group would benefit from more representation from async ecosystem projects. This is an example of a larger piece of Rust "organizational debt", where the term "working group" has been used for many different purposes over the years.
In the second half of 2024 we are planning on the following work items. The following three items are what we consider to be the highest priority, as they do the most to lay a foundation for future progress (and they themselves are listed in priority order):
- resolve the "Send bound" problem, which blocks the widespread usage of async functions in traits;
- reorganize the async WG, so that we can be better aligned and move more swiftly from here out;
- stabilize async closures, allowing for a much wider variety of async related APIs (async closures are implemented on nightly).
We have also identified two "stretch goals" that we believe could be completed:
- stabilize trait for async iteration
- complete async drop experiments (currently unfunded)
Resolve the "send bound" problem
Although async functions in traits were stabilized, there is currently no way to write a generic function that requires impls where the returned futures are Send
. This blocks the use of async function in traits in some core ecosystem crates, such as tower, which want to work across all kinds of async executors. This problem is called the "send bound" problem and there has been extensive discussion of the various ways to solve it. RFC #3654 has been opened proposing one solution and describing why that path is preferred. Our goal for the year is to adopt some solution on stable.
We plan to reorganize the async working group into a structure that will better serve the projects needs, especially when it comes to aligning around a clear async vision. In so doing, we will help "launch" the async working group out from the launchpad umbrella team and into a more permanent structure.
Despite its limitations, the async working group serves several important functions for async Rust that need to continue:
- It provides a forum for discussion around async-related topics, including the
#async-wg
zulip stream as well as regular sync meetings. These forums don't necessarily get participation by the full set of voices that we would like, however. - It owns async-related repositories, such as the sources for the async Rust book (in dire need of improvement), arewewebyet, and the futures-rs crate. Maintenance of these sites has varied though and often been done by a few individuals acting largely independently.
- It advises the more general teams (typically Lang and Libs-API) on async-related matters. The authoring of the (mildly dated) async vision doc took place under the auspices of the working group, for example. However, the group lacks decision making power and doesn't have a strong incentive to coallesce behind a shared vision, so it remains more a "set of individual voices" that does not provide the general purpose teams with clear guidance.
We plan to propose one or more permanent teams to meet these same set of needs. The expectation is that these will be subteams under the Lang and Libs top-level teams.
Building ergonomic APIs in async is often blocked by the lack of async closures. Async combinator-like APIs today typically make use of an ordinary Rust closure that returns a future, such as the filter
API from StreamExt
:
fn filter<Fut, F>(self, f: F) -> Filter<Self, Fut, F>
where
F: FnMut(&Self::Item) -> Fut,
Fut: Future<Output = bool>,
Self: Sized,
This approach however does not allow the closure to access variables captured by reference from its environment:
let mut accept_list = vec!["foo", "bar"]
stream
.filter(|s| async { accept_list.contains(s) })
The reason is that data captured from the environment is stored in self
. But the signature for sync closures does not permit the return value (Self::Output
) to borrow from self
:
trait FnMut<A>: FnOnce<A> {
fn call_mut(&mut self, args: A) -> Self::Output;
}
To support natural async closures, a trait is needed where call_mut
is an async fn
, which would allow the returned future to borrow from self
and hence modify the environment (e.g., accept_list
, in our example above). Or, desugared, something that is equivalent to:
trait AsyncFnMut<A>: AsyncFnOnce<A> {
fn call_mut<'s>(
&'s mut self,
args: A
) -> impl Future<Output = Self::Output> + use<'s, A>;
// ^^^^^^^^^^ note that this captures `'s`
//
// (This precise capturing syntax is unstable and covered by
// rust-lang/rust#123432).
}
The goal for this year to be able to
- support some "async equivalent" to
Fn
,FnMut
, andFnOnce
bounds- this should be usable in all the usual places
- support some way to author async closure expressions
These features should be sufficient to support methods like filter
above.
The details (syntax, precise semantics) will be determined via experimentation and subject to RFC.
There has been extensive discussion about the best form of the trait for async iteration (sometimes called Stream
, sometimes AsyncIter
, and now being called AsyncGen
). We believe the design space has been sufficiently explored that it should be possible to author an RFC laying out the options and proposing a specific plan.
Authors of async code frequently need to call async functions as part of resource cleanup. Because Rust today only supports synchronous destructors, this cleanup must take place using alternative mechanisms, forcing a divergence between sync Rust (which uses destructors to arrange cleanup) and async Rust. MCP 727 proposed a series of experiments aimed at supporting async drop in the compiler. We would like to continue and complete those experiments. These experiments are aimed at defining how support for async drop will be implemented in the compiler and some possible ways that we could modify the type system to support it (in particular, one key question is how to prevent types whose drop is async from being dropped in sync code).
Our eventual goal is to provide Rust users building on async with
- the same core language capabilities as sync Rust (async traits with dyn dispatch, async closures, async drop, etc);
- reliable and standardized abstractions for async control flow (streams of data, error recovery, concurrent execution), free of accidental complexity;
- an easy "getting started" experience that builds on a rich ecosystem;
- good performance by default, peak performance with tuning;
- the ability to easily adopt custom runtimes when needed for particular environments, language interop, or specific business needs.
- Uphold sync Rust's bar for reliability. Sync Rust famously delivers on the general feeling of "if it compiles, it works" -- async Rust should do the same.
- We lay the foundations for a thriving ecosystem. The role of the Rust org is to develop the rudiments that support an interoperable and thriving async crates.io ecosystem.
- When in doubt, zero-cost is our compass. Many of Rust's biggest users are choosing it becase they know it can deliver the same performnace (or better) than C. If we adopt abstractions that add overhead, we are compromising that core strength. As we build out our designs, we ensure that they don't introduce an "abstraction tax" for using them.
- From embedded to GUI to the cloud. Async Rust covers a wide variety of use cases and we aim to make designs that can span those differing constraints with ease.
- Consistent, incremental progress. People are building async Rust systems today -- we need to ship incremental improvements while also steering towards the overall outcome we want.
Here is a detailed list of the work to be done and who is expected to do it. This table includes the work to be done by owners and the work to be done by Rust teams (subject to approval by the team in an RFC/FCP). The overall owners of the async effort (and authors of this goal document) are tmandry and nikomatsakis. We have identified owners for subitems below; these may change over time.
- The badge indicates that the owner has committed and work will be funded by their employer or other sources.
- The badge indicates that there is a willing owner but they need funding to pursue the goal. Depending on the owner's individual circumstances, this could be support/authorization from their employer, grants, or contracting.
- The badge indicates a requirement where Team support is needed.
Subgoal | Owner(s) or team(s) | Status |
---|---|---|
overall program management | tmandry, nikomatsakis | |
resolve the "send bound" problem | ||
↳ |
||
↳ |
nikomatsakis | |
↳ approve RTN RFC or provide alternative | Lang | (in FCP) |
↳ stabilization | compiler-errors | |
reorganize the async WG | ||
↳ author proposal | tmandry, nikomatsakis | |
↳ approve changes to team structure | Libs, Lang | |
stabilize async closures | ||
↳ |
||
↳ author RFC | nikomatsakis or compiler-errors | |
↳ approve RFC | Lang | |
↳ stabilization | compiler-errors | |
stabilize trait for async iteration | ||
↳ author RFC | eholk | |
↳ approve RFC | Libs-API | |
↳ implementation | eholk | |
complete async drop experiments | ||
↳ |
||
↳ |
||
↳ implementation work | petrochenkov | (*) |
(*) Implementation work on async drop experiments is currently unfunded. We are trying to figure out next steps.
Agreement from Lang, Libs Libs-API to prioritize the items marked in the table above.
The expectation is that
- async closures will occupy 2 design meetings from lang during H2
- async iteration will occupy 2 design meetings from lang during H2 and likely 1-2 from libs API
- misc matters will occupy 1 design meeting from lang during H2
for a total of 4-5 meetings from lang and 1-2 from libs API.
This is an ambitious agenda, no doubt. We believe it is possible if the teams are behind us, but things always take longer than you think. We have made sure to document the "priority order" of items for this reason. We intend to focus our attention first and foremost on the high priority items.
These are the two features that together block the authoring of traits for a number of common interop purposes. Send bounds are needed for generic traits like the Service
trait. Async closures are needed for rich combinator APIs like iterators.
Async fn in traits do not currently support native dynamic dispatch. We have explored a number of designs for making it work but are not currently prioritizing that effort. It was determined that this idea is lower priority because it is possible to workaround the gap by having the #[trait_variant]
crate produce a dynamic dispatch wrapper type (e.g., #[trait_variant(dyn = DynWidget)] trait Widget
would create a type DynWidget<'_>
that acts like a Box<dyn Widget>
). We do expect to support dyn async trait, hopefully in 2025.
There has been extensive discussion about the best design for the "Stream" or "async iter" trait and we judge that the design space is well understood. We would like to unblock generator syntax in 2025 which will require some form of trait.
The majority of the debate about the trait has been on the topic of whether to base the trait on a poll_next
function, as we do today, or to try and make the trait use async fn next
, making it more anaologous with the Iterator
trait (and potentially even making it be two versions of a single trait). We will definitely explore forwards compatibility questions as part of this discussion. nikomatsakis for example still wants to explore maybe-async-like designs, especially for combinator APIs like map
. However, we also refer to the design axiom that "when in doubt, zero-cost is our compass" -- we believe we should be able to stabilize a trait that does the low-level details right, and then design higher level APIs atop that.
Yes, we do, and the [existing document][avd] has been very helpful in understanding the space. Moreover, that document was never RFC'd and we have found that it lacks a certain measure of "authority" as a result. We would like to drive stronger alignment on the path forward so that we can focus more on execution. But doing that is blocked on having a more effective async working group structure (hence the goal to reorganize the async WG).
Keyword generics is an ambitious initiative to enable code that is "maybe async". It has generated significant controversy, with some people feeling it is necessary for Rust to scale and others judging it to be overly complex. One of the reasons to reorganize the async WG is to help us come to a consensus around this point (though this topic is broader than async).