owned this note
owned this note
Published
Linked with GitHub
# Async Shiny Future Design Doc Sketches and Notes
## Use async fn anywhere
High-level goal: One should be able to write `async fn` anywhere that you can write `fn`.
### Type alias impl trait (TAIT)
* **Status:** Development, close to stabilization
* Permits `type Foo = impl Trait` at module and impl level
* Within the defining scope of `Foo` (module or impl, respectively), the TAIT `Foo` must be used only in certain positions:
* fn return types
* value of an associated type
* [Stabilization report](https://hackmd.io/o9KO-D8aSb6bJgQ1froVTA)
* [Project board](https://github.com/rust-lang/wg-traits/projects/4)
### Generic associated types
* **Status:** Development, "approaching" stabilization
* Permits `type Foo<...>` in traits and impls
* [Project board](https://github.com/rust-lang/wg-traits/projects/4)
### Async fn in traits
You should be able to write `async fn` in traits and impls:
```rust
trait Foo {
async fn bar(&self);
}
impl Foo for MyType {
async fn bar(&self) {
...
}
}
```
This desugars into a GAT + impl Trait:
```rust
trait Foo {
type Bar<'me>: Future + 'me;
fn bar(&self) -> Self::Bar<'_>;
}
impl Foo for MyType {
type Bar<'me> = impl Future + 'me;
fn bar(&self) -> Self::Bar<'_> {
async move {
...
}
}
}
```
Unresolved design questions:
* What is the name of that GAT we introduce?
* I called it `Bar` here, but that's somewhat arbitrary, perhaps we want to have some generic syntax for naming the method?
* Or for getting the type of the method.
* This problem applies equally to other "`-> impl Trait` in trait" scenarios.
* [Exploration doc](https://hackmd.io/IISsYc0fTGSSm2MiMqby4A)
* Can users easily bound those GATs with `Send`, maybe even in the trait definition?
* People are likely to want to say "I want every future produced by this trait to be Send", and right now that is quite tedious.
* This applies equally to other "`-> impl Trait` in trait" scenarios.
* What about "dyn" traits?
* See the sections on "inline" and dyn" async fn in traits below!
### "Inline" async fn in traits
Short version: make it possible to have async fn where the state is stored in the `Self` type ([detailed writeup](https://hackmd.io/bKfiVPRpTvyX8JK_Ng2EWA)). This is equivalent to writing a poll function. Like a poll function, it makes the trait dyn safe; it also has the advantage that `Self: Send` implies that the returned future is also `Send`.
Remaining challenges:
* Primarily bikeshed. How should we designate that an async function is 'inline', and can we come up with a less overloaded name?
### "Dyn" async fn in traits
The most basic desugaring of async fn in traits will make the trait not dyn-safe. "Inline" async fn in traits is one way to circumvent that, but it's not suitable for all traits that must be dyn-safe. There are other efficient options:
* Return a `Box<dyn Async<...>>` -- but then we must decide if it will be `Send`, right? And we'd like to only do that when using the trait as a `dyn Trait`. Plus it is not compatible with no-std.
* This comes down to needing some form of opt-in.
This concern applies equally to other "`-> impl Trait` in trait" scenarios.
We have looked at revising how "dyn traits" are handled more generally in the lang team on a number of occasions, but [this meeting](https://github.com/rust-lang/lang-team/blob/master/design-meeting-minutes/2020-01-13-dyn-trait-and-coherence.md) seems particularly relevant. In that meeting we were discussing some soundness challenges with the existing dyn trait setup and discussing how some of the directions we might go enabled folks to write their *own* `impl Trait for dyn Trait` impls, thus defining for themselves how the mapping from Trait to dyn Trait. This seems like a key piece of the solution.
One viable route might be:
* Traits using `async fn` are not, by default, dyn safe.
* You can declare how you want it to be dyn safe:
* `#[repr(inline)]`
* or `#[derive(dyn_async_boxed)]` or some such
* to take an `#[async_trait]`-style approach
* It would be nice if users can declare their own styles. For example, Matthias247 pointed out that the `Box` used to allocate can be reused in between calls for increased efficiency.
* It would also be nice if there's an easy, decent default -- maybe you don't even *have* to opt-in to it if you are not in `no_std` land.
### Recursive async functions
Recursive async functions are not currently possible. This is an artifact of how async fns work today: they allocate all the stack space they will ever need in one shot, which cannot be known for recursive functions.
The compiler could manage this by inserting a "box" automatically (perhaps with an allow-by-default lint to let you know it's happening); another option would be to have a convenient way to make a "boxed" async fn, such as `box async fn`, and the compiler could suggest inserting this keyword at the appropriate point so that such a function *can* be recursive.
(Note that the boxed function would not have to be a `Box<dyn Async>`, it could be a nominal type instead, and thus the only runtime overhead comes from memory allocation.)
*Alternatives:* If we were very concerned about this, we could conceivably switch to an optional "arena" for growing the stack, but this scenario seems to come up relatively rarely.
## Easily compose, control schedule
Provide a new "building block" for scheduling based on hierarchical futures. This building block should:
* Easily permit spawning concurrent or parallel tasks such that the runtime is able to poll them
### New async trait
Today's `Future` trait lacks one fundamental capability compared to synchronous code: there is no way to "block" your caller and be sure that the caller will not continue executing until you agree. In synchronous code, you can use a closure and a destructor to achieve this, which is the technique used for things like `rayon::scope` and crossbeam's scoped threads.
Async functions are commonly written with borrowed references as arguments:
```rust
async fn do_something(db: &Db) { ... }
```
but important utilities like `spawn` and `spawn_blocking` require `'static` tasks. Without "non-cancelable" traits, the only way to circumvent this is with mechanisms like `FuturesUnordered`. Fundamentally the challenge is that
Introduce a trait like
```rust
trait Async {
type Output;
/// # Unsafe conditions
///
/// * Once polled, cannot be moved
/// * Once polled, destructor must execute before memory is deallocated
/// * Once polled, must be polled to completion
unsafe fn poll(
&mut self,
context: &mut core::task::Context<'_>,
) -> core::task::Ready<Self::Output>;
/// Requests cancellation; poll must still be called.
fn request_cancellation(
&mut self
);
}
```
### Specialization to bridge old and new traits
Introduce "bridge impls" like the following:
```rust
impl<F> Async for F where F: Future {
}
```
However, we also need the ability for common combinators to implement both `Future` and `Αsync`:
```rust
struct Join<A, B> { ... }
impl<A, B> Future for Join<A, B>
where
A: Future,
B: Future,
{ }
impl<A, B> Async for Join<A, B>
where
A: Async,
B: Async,
{ }
```
This creates a problem, because you have multiple routes to implement `Async` for `Join<A, B>` where `A` and `B` are futures.
Specialization can be used to resolve this, and it would be a great feature for Rust overall. However, specialization has a number of challenges to overcome. Some related articles:
* [Maximally minimal specialization](https://smallcultfollowing.com/babysteps/blog/2018/02/09/maximally-minimal-specialization-always-applicable-impls/)
* [Supporting blanket impls in specialization](https://smallcultfollowing.com/babysteps/blog/2016/10/24/supporting-blanket-impls-in-specialization/)
### Scoped-based API
Async functions are commonly written with borrowed references as arguments:
```rust
async fn do_something(db: &Db) { ... }
```
but important utilities like `spawn` and `spawn_blocking` require `'static` tasks. Building on non-cancelable traits, we can implement a "scope" API that allows one to introduce an async scope. This scope API should permit one to spawn tasks into a scope, but have various kinds of scopes (e.g., synchronous execution, parallel execution, and so forth). It should ultimately reside in the standard library and hook into different runtimes for scheduling. This will take some experimentation!
```rust
let result = std::async_thread::scope(|s| {
let job1 = s.spawn(async || {
11
});
let job2 = s.spawn_blocking(|| {
11
});
job1.await + job2.await
}).await;
assert_eq!(result, 22);
```
#### Side-stepping the nested await problem
One goal of scopes is to avoid the "nested await" problem, as described in [Barbara battles buffered streams (BBBS)][BBBS]. The idea is like this: any combinator which merges multiple async pieces of work -- i.e., initiates concurrency -- needs to take a scope parameter. The way to get concurrency, in other words, is to spawn into a scope, and not to construct small "subschedulers". This includes `FuturesUnordered` and `Stream::buffered`, but also more familiar APIs like `join`. By doing this, we ensure that the scheduler is able to poll those concurrent tasks even while the main task is busy doing something else. A good rule of thumb here is that *only the scheduler ever invokes poll*. **All other code just "awaits" things.**[^hard]
[^hard]: This is not a hard rule. But invoking poll manually is best regarded as a risky thing to be managed with care -- not only because of the formal safety guarantees, but because of the possibility for "nested await"-style failures.
[BBBS]: https://rust-lang.github.io/wg-async-foundations/vision/status_quo/barbara_battles_buffered_streams.html
[`buffered`]: https://docs.rs/futures/0.3.15/futures/prelude/stream/trait.StreamExt.html#method.buffered
In the case of [BBBS], the problem arises because of `buffered`, which spawns off concurrent work to process multiple connections. This means that the [`buffered`] combinator would take a `scope` parameter to use for spawning:
```rust
async fn do_work(database: &Database) {
std::async_thread::scope(|s| {
let work = do_select(database, FIND_WORK_QUERY)?;
std::async_iter::from_iter(work)
.map(|item| do_select(database, work_from_item(item)))
.buffered(5, scope)
.for_each(|work_item| process_work_item(database, work_item))
.await;
}).await;
}
```
The `join` combinator would likely be replaced with a method on `scope`:[^variadic]
[^variadic]: Naturally we would want a variadic variation, or perhaps a method macro.
```rust
let (a_result, b_result) = scope.join(a, b).await;
```
This join method would be defined like so:
```rust
impl Scope<'s> {
async fn join<A, B>(&mut self, a: A, b: B) -> (A::Output, B::Output)
where
A: Async + 's,
B: Async + 's,
{
(self.spawn(a).await, b.await)
}
}
```
#### Could there be a convenient way to access the current scope?
If we wanted to integrate the idea of scopes more deeply, we could have some way to get access to the current scope and reference its lifetime. Let's imagine we added a keyword `scope`, and we said that its lifetime is `'scope`. One would then be able to do something like the following:
Lots of unknowns to work out here, though. For example, suppose you have a function that creates a scope and invokes a closure within. Do we have a way to indicate to the closure that `'scope` in that closure may be different?
It starts to feel like simply passing "scope" values may be simpler, and perhaps we need a way to automate the threading of state instead.
### Voluntary cancellation
In today's Rust, any async function can be synchronously cancelled at any await point: the code simply stops executing, and destructors are run for any extant variables. This leads to a lot of bugs. (TODO: link to stories)
Under systems like [Swift's proposed structured concurrency model](https://github.com/apple/swift-evolution/blob/main/proposals/0304-structured-concurrency.md), or with APIs like [.NET's CancellationToken](https://docs.microsoft.com/en-us/dotnet/api/system.threading.cancellationtoken?view=net-5.0), cancellation is "voluntary". What this means is that when a task is cancelled, a flag is set; the task can query this flag but is not otherwise affected. Under structured concurrency systems, this flag is propagated to all chidren (and transitively to their children).
[preemption]: https://tokio.rs/blog/2020-04-preemption
Voluntary cancellation is a requirement for scopes. If there are parallel tasks executing within a scope, and the scope itself is canceled, those parallel tasks must be joined and halted before the memory for the scope can be freed.
One downside of such a system is that cancellation *may not* take effect. We can make it more likely to work by integrating the cancellation flag into the standard library methods, similar to how tokio encourages ["voluntary preemption"][preemption]. This means that file reads and things will start to report errors (`Err(TaskCanceled)`) once the task has been canceled. This has the advantage that it exercises existing error paths and permits recovery.
#### Cancellation and `select`
The `select` macro chooses from N futures and returns the first one that matches. Today, the others are immediately canceled. This behavior doesn't play especially well with voluntary cancellation. There are a few options here:
* We could make `select` signal cancellation for each of the things it is selecting over and then wait for them to finish.
* We could also make `select` continue to take `Future` (not `Async`) values, which effectively makes `Future` a "cancel-safe" trait (or perhaps we introduce a `CancelSafe` marker trait that extends `Async`).
* This would mean that typical `async fn` could not be given to select, though we might allow people to mark `async fn` as "cancel-safe", in which case they would implement `Future`. They would also not have access to ordinary async fn, though.
* Of course, users could spawn a task that calls the function and give the handle to select.
### Async drop
Create an `AsyncDrop` trait:
```rust
#[repr(inline)]
trait AsyncDrop {
async fn async_drop(&mut self);
}
```
When an async function is compiled and a value is dropped, we will use the "async drop glue". This is analogous to drop glue except that it invokes the `foo.async_drop().await` method where appropriate.
There is also a lint for when something that implements `AsyncDrop` is dropped in a synchronous context.
#### What happens when you drop an async-drop value in a sync context?
We can't stop you from doing that right now and I don't want to encumber this work with trying to crack that nut. (The basic idea would be to allow things to `impl !Drop for X`, and to make `T: Drop` a default rather like `Sized`, but there's lots of details to work out.) Instead, we offer a lint, and we would encourage people implementing `AsyncDrop` to abort or warn or otherwise try to recover as gracefully as they can. The hope is that this is "sufficiently reliable" that the scenario doesn't happen a lot in practice. Not especially satisfying.
## Generic code that is portable across runtimes
### Read and write
We need to have `AsyncRead` and `AsyncWrite` traits. These have several design goals:
* No poll functions directly exposed (use `async fn`)
* Be accessible via `dyn` trait
* Permit simultaneous reads/writes
One possibility is the design that [CarlLerche proposed](https://gist.github.com/carllerche/5d7037bd55dac1cb72891529a4ff1540), which separates "readiness" from the actual (non-async) methods to acquire the data:
```rust
pub struct Interest(...);
pub struct Ready(...);
impl Interest {
pub const READ = ...;
pub const WRITE = ...;
}
#[repr(inline)]
pub trait AsyncIo {
/// Wait for any of the requested input, returns the actual readiness.
///
/// # Examples
///
/// ```
/// async fn main() -> Result<(), Box<dyn Error>> {
/// let stream = TcpStream::connect("127.0.0.1:8080").await?;
///
/// loop {
/// let ready = stream.ready(Interest::READABLE | Interest::WRITABLE).await?;
///
/// if ready.is_readable() {
/// let mut data = vec![0; 1024];
/// // Try to read data, this may still fail with `WouldBlock`
/// // if the readiness event is a false positive.
/// match stream.try_read(&mut data) {
/// Ok(n) => {
/// println!("read {} bytes", n);
/// }
/// Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => {
/// continue;
/// }
/// Err(e) => {
/// return Err(e.into());
/// }
/// }
///
/// }
///
/// if ready.is_writable() {
/// // Try to write data, this may still fail with `WouldBlock`
/// // if the readiness event is a false positive.
/// match stream.try_write(b"hello world") {
/// Ok(n) => {
/// println!("write {} bytes", n);
/// }
/// Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => {
/// continue
/// }
/// Err(e) => {
/// return Err(e.into());
/// }
/// }
/// }
/// }
/// }
/// ```
async fn ready(&mut self, interest: Interest) -> io::Result<Ready>;
}
pub trait AsyncRead: AsyncIo {
fn try_read(&mut self, buf: &mut ReadBuf<'_>) -> io::Result<()>;
}
pub trait AsyncWrite: AsyncIo {
fn try_write(&mut self, buf: &[u8]) -> io::Result<usize>;
}
```
This allows users to:
* Take `T: AsyncRead`, `T: AsyncWrite`, or `T: AsyncRead + AsyncWrite`
Note that it is always possible to ask whether writes are "ready", even for a read-only source; the answer will just be "no" (or perhaps an error).
### Iterator
The async iterator trait, like async read and write, can leverage "dyn in traits":
## Use current runtime without generics
### std library spawn
### std library async_io
### std async abstractions
## Open questions and far out ideas
### async as a monomorphization mode
Niko has been toying with the far out idea of making it so that any `fn` can be compiled as an "async fn". Put another way, what if there was an implicit "mode" for your function. It could be compiled in synchronous mode *or* asynchronous mode? There would be some functions that are only compatible with one or the other, but a lot of things (e.g., the I/O abstractions in the standard library) could be made compatible with both.
What could be amazing here is that it might offer a way for us to do all kinds of things:
* To ease the sync/async split
* To support `?` inside of iterator combinators (by making the intermediate functions async-- perhaps cancel-safe async)
Some challenges:
* At some point, we have to detect that your function invokes a "sync only" function and thus cannot be used in an async context. When do we do that? We could do it at monomorphization time, but we've shied away from that.
* If we try to do it before monomorphization time, though, are we back to where we started?
* This is basically an effect system.
### await syntax
Should we require you to use `.await`? After the epic syntax debates we had, wouldn't it be ironic if we got rid of it altogether, as [carllerche has proposed](https://carllerche.com/2021/06/17/six-ways-to-make-async-rust-easier/)?
Basic idea:
* When you invoke an async function, it could await by default.
* You would write `async foo()` to create an "async expression" -- i.e., to get a `impl Async`.
* You might instead write `async || foo()`, i.e., create an async closure.
Appealing characteristics:
* **More analogous to sync code.** In sync code, if you want to defer immediately executing something, you make a closure. Same in async code, but it's an async closure.
* **No confusion around remembering to await.** Right now the compiler has to go to some lengths to offer you messages suggesting you insert `.await`. It'd be nice if you just didn't have to remember.
* **Room for optimization.** When you first invoke an async function, it can immediately start executing; it only needs to create a future in the event that it suspends. This may also make closures somewhat smaller.
* This could be partially achieved by adding an optional method on the trait that compiles a version of the fn meant to be used when it is *immediately awaited*.
But there are some downsides:
* **Churn.** Introducing a new future trait is largely invisible to users except in that it manifests as version mismatches. Removing the await keyword is a much more visible change.
* **Await points are less visible.** There may be opportunity to introduce concurrency and so forth that is harder to spot when reading the code, particularly outside of an IDE. (In Kotlin, which adopts this model, suspend points are visible in the "gutter" of the editor, but this is not visible when reviewing patches on github.)
* **Async becomes an effect.** In today's Rust, an "async function" desugars into a traditional function that returns a future. This function is called like any other, and hence it can implement the `Fn` traits and so forth. In this "await-less" Rust, an async function is called differently from other functions, because it induces an await. This means that we need to consider `async` as a kind of "effect" (like `unsafe`) in a way that is not today.
### ?Drop
One problem with async drop is that nothing stops you from dropping async values in a sync context. What do we do in that scenario? There isn't a great answer. The problem is that, right now, Rust assumes that *all* values are "droppable". But what if we removed that assumption. You could declare some values as *non-droppable* (e.g., `impl !Drop for Foo`). This would then mean that those values *must* be consumed in some way, presumably via some kind of blessed function that takes ownership of the value. Generic code would then not be able to use those functions unless it was declared as `T: ?Drop`. That's right, another `?` trait -- something we have traditionally shied away from (and with good reason).
Lots of sticky questions to answer. What about `dyn Trait`, for example?