rfcs/text/2033-experimental-coroutine...

495 lines
22 KiB
Markdown

- Feature Name: `coroutines`
- Start Date: 2017-06-15
- RFC PR: [rust-lang/rfcs#2033](https://github.com/rust-lang/rfcs/pull/2033)
- Rust Issue: [rust-lang/rust#43122](https://github.com/rust-lang/rust/issues/43122)
# Summary
[summary]: #summary
This is an **experimental RFC** for adding a new feature to the language,
coroutines (also commonly referred to as generators). This RFC is intended to be
relatively lightweight and bikeshed free as it will be followed by a separate
RFC in the future for stabilization of this language feature. The intention here
is to make sure everyone's on board with the *general idea* of
coroutines/generators being added to the Rust compiler and available for use on
the nightly channel.
# Motivation
[motivation]: #motivation
One of Rust's 2017 roadmap goals is ["Rust should be well-equipped for writing
robust, high-scale servers"][goal]. A [recent survey][survey] has shown that
the biggest blocker to robust, high-scale servers is ergonomic usage of async
I/O (futures/Tokio/etc). Namely, the lack of async/await syntax. Syntax like
async/await is essentially the defacto standard nowadays when working with async
I/O, especially in languages like C#, JS, and Python. Adding such a feature to
rust would be a huge boon to productivity on the server and make significant
progress on the 2017 roadmap goal as one of the largest pain points, creating
and returning futures, should be as natural as writing blocking code.
[goal]: https://github.com/rust-lang/rfcs/blob/master/text/1774-roadmap-2017.md#rust-should-be-well-equipped-for-writing-robust-high-scale-servers
[survey]: https://users.rust-lang.org/t/what-does-rust-need-today-for-server-workloads/11114
With our eyes set on async/await the next question is how would we actually
implement this? There's sort of two main sub-questions that we have to answer to
make progress here though, which are:
* What's the actual syntax for async/await? Should we be using new keywords in
the language or pursuing syntax extensions instead?
* How do futures created with async/await support suspension? Essentially while
you're waiting for some sub-future to complete, how does the future created by
the async/await syntax return back up the stack and support coming back and
continuing to execute?
The focus of this experimental RFC is predominately on the second, but before we
dive into more motivation there it may be worth to review the expected syntax
for async/await.
### Async/await syntax
Currently it's intended that **no new keywords are added to
Rust yet** to support async/await. This is done for a number of reasons, but
one of the most important is flexibility. It allows us to stabilize features
more quickly and experiment more quickly as well.
Without keywords the intention is that async/await will be implemented with
macros, both procedural and `macro_rules!` style. We should be able to leverage
[procedural macros][pmac] to give a near-native experience. Note that procedural
macros are only available on the nightly channel today, so this means that
"stable async/await" will have to wait for procedural macros (or at least a
small slice) to stabilize.
[pmac]: https://github.com/rust-lang/rfcs/blob/master/text/1566-proc-macros.md
With that in mind, the expected syntax for async/await is:
```rust
#[async]
fn print_lines() -> io::Result<()> {
let addr = "127.0.0.1:8080".parse().unwrap();
let tcp = await!(TcpStream::connect(&addr))?;
let io = BufReader::new(tcp);
#[async]
for line in io.lines() {
println!("{}", line);
}
Ok(())
}
```
The notable pieces here are:
* `#[async]` is how you tag a function as "this returns a future". This is
implemented with a `proc_macro_attribute` directive and allows us to change
the function to actually returning a future instead of a `Result`.
* `await!` is usable inside of an `#[async]` function to block on a future. The
`TcpStream::connect` function here can be thought of as returning a future of
a connected TCP stream, and `await!` will block execution of the `print_lines`
function until it becomes available. Note the trailing `?` propagates errors
as the `?` does today.
* Finally we can implement more goodies like `#[async]` `for` loops which
operate over the `Stream` trait in the `futures` crate. You could also imagine
pieces like `async!` blocks which are akin to `catch` for `?`.
The intention with this syntax is to be as familiar as possible to existing Rust
programmers and disturb control flow as little as possible. To that end all
that's needed is to tag functions that may block (e.g. return a future) with
`#[async]` and then use `await!` internally whenever blocking is needed.
Another critical detail here is that the API exposed by async/await is quite
minimal! You'll note that this RFC is an experimental RFC for coroutines and we
haven't mentioned coroutines at all with the syntax! This is an intentional
design decision to keep the implementation of `#[async]` and `await!` as
flexible as possible.
### Suspending in async/await
With a rough syntax in mind the next question was how do we actually suspend
these futures? The function above will desugar to:
```rust
fn print_lines() -> impl Future<Item = (), Error = io::Error> {
// ...
}
```
and this means that we need to create a `Future` *somehow*. If written with
combinators today we might desugar this to:
```rust
fn print_lines() -> impl Future<Item = (), Error = io::Error> {
lazy(|| {
let addr = "127.0.0.1:8080".parse().unwrap();
TcpStream::connect(&addr).and_then(|tcp| {
let io = BufReader::new(tcp);
io.lines().for_each(|line| {
println!("{}", line);
Ok(())
})
})
})
}
```
Unfortunately this is actually quite a difficult transformation to do
(translating to combinators) and it's actually not quite as optimal as we might
like! We can see here though some important points about the semantics that we
expect:
* When called, `print_lines` doesn't actually do anything. It immediately just
returns a future, in this case created via [`lazy`].
* When `Future::poll` is first called, it'll create the `addr` and then call
`TcpStream::connect`. Further calls to `Future::poll` will then delegate to
the future returned by `TcpStream::connect`.
* After we've connected (the `connect` future resolves) we continue our
execution with further combinators, blocking on each line being read from the
socket.
[`lazy`]: https://docs.rs/futures/0.1.14/futures/future/fn.lazy.html
A major benefit of the desugaring above is that there are no hidden allocations.
Combinators like `lazy`, `and_then`, and `for_each` don't add that sort of
overhead. A problem, however, is that there's a bunch of nested state machines
here (each combinator is its own state machine). This means that our in-memory
representation can be a bit larger than it needs to be and take some time to
traverse. Finally, this is also very difficult for an `#[async]` implementation
to generate! It's unclear how, with unusual control flow, you'd implement all
the paradigms.
Before we go on to our final solution below it's worth pointing out that a
popular solution to this problem of generating a future is to side step
this completely with the concept of green threads. With a green thread you can
suspend a thread by simply context switching away and there's no need to
generate state and such as an allocated stack implicitly holds all this state.
While this does indeed solve our problem of "how do we translate `#[async]`
functions" it unfortunately violates Rust's general theme of "zero cost
abstractions" because the allocated stack on the side can be quite costly.
At this point we've got some decent syntax and rough (albeit hard) way we want
to translate our `#[async]` functions into futures. We've also ruled out
traditional solutions like green threads due to their costs, so we just need a
way to easily create the optimal state machine for a future that combinators
would otherwise emulate.
### State machines as "stackless coroutines"
Up to this point we haven't actually mentioned coroutines all that much which
after all is the purpose of this RFC! The intention of the above motivation,
however, is to provide a strong case for *why coroutines?* At this point,
though, this RFC will mostly do a lot of hand-waving. It should suffice to say,
though, that the feature of "stackless coroutines" in the compiler is precisely
targeted at generating the state machine we wanted to write by hand above,
solving our problem!
Coroutines are, however, a little lower level than futures themselves. The
stackless coroutine feature can be used not only for futures but also other
language primitives like iterators. As a result let's take a look at what a
hypothetical translation of our original `#[async]` function might look like.
Keep in mind that this is not a specification of syntax, it's just a strawman
possibility for how we'd write the above.
```rust
fn print_lines() -> impl Future<Item = (), Error = io::Error> {
CoroutineToFuture(|| {
let addr = "127.0.0.1:8080".parse().unwrap();
let tcp = {
let mut future = TcpStream::connect(&addr);
loop {
match future.poll() {
Ok(Async::Ready(e)) => break Ok(e),
Ok(Async::NotReady) => yield,
Err(e) => break Err(e),
}
}
}?;
let io = BufReader::new(tcp);
let mut stream = io.lines();
loop {
let line = {
match stream.poll()? {
Async::Ready(Some(e)) => e,
Async::Ready(None) => break,
Async::NotReady => {
yield;
continue
}
}
};
println!("{}", line);
}
Ok(())
})
}
```
The most prominent addition here is the usage of `yield` keywords. These are
inserted here to inform the compiler that the coroutine should be suspended for
later resumption. Here this happens precisely where futures are themselves
`NotReady`. Note, though, that we're not working directly with futures (we're
working with coroutines!). That leads us to this funky `CoroutineToFuture` which
might look like so:
```rust
struct CoroutineToFuture<T>(T);
impl<T: Coroutine> Future for CoroutineToFuture {
type Item = T::Item;
type Error = T::Error;
fn poll(&mut self) -> Poll<T::Item, T::Error> {
match Coroutine::resume(&mut self.0) {
CoroutineStatus::Return(Ok(result)) => Ok(Async::Ready(result)),
CoroutineStatus::Return(Err(e)) => Err(e),
CoroutineStatus::Yield => Ok(Async::NotReady),
}
}
}
```
Note that some details here are elided, but the basic idea is that we can pretty
easily translate all coroutines into futures through a small adapter struct.
As you may be able to tell by this point, we've now solved our problem of code
generation! This last transformation of `#[async]` to coroutines is much more
straightforward than the translations above, and has in fact [already been
implemented][futures-await].
[futures-await]: https://github.com/alexcrichton/futures-await
To reiterate where we are at this point, here's some of the highlights:
* One of Rust's roadmap goals for 2017 is pushing Rust's usage on the server.
* A major part of this goal is going to be implementing async/await syntax for
Rust with futures.
* The async/await syntax has a relatively straightforward syntactic definition
(borrowed from other languages) with procedural macros.
* The procedural macro itself can produce optimal futures through the usage of
*stackless coroutines*
Put another way: if the compiler implements stackless coroutines as a feature,
we have now achieved async/await syntax!
### Features of stackless coroutines
At this point we'll start to tone down the emphasis of servers and async I/O
when talking about stackless coroutines. It's important to keep them in mind
though as motivation for coroutines as they guide the design constraints of
coroutines in the compiler.
At a high-level, though, stackless coroutines in the compiler would be
implemented as:
* No implicit memory allocation
* Coroutines are translated to state machines internally by the compiler
* The standard library has the traits/types necessary to support the coroutines
language feature.
Beyond this, though, there aren't many other constraints at this time. Note that
a critical feature of async/await is that **the syntax of stackless coroutines
isn't all that important**. In other words, the implementation detail of
coroutines isn't actually exposed through the `#[async]` and `await!`
definitions above. They purely operate with `Future` and simply work internally
with coroutines. This means that if we can all broadly agree on async/await
there's no need to bikeshed and delay coroutines. Any implementation of
coroutines should be easily adaptable to async/await syntax.
# Detailed design
[design]: #detailed-design
Alright hopefully now we're all pumped to get coroutines into the compiler so we
can start playing around with async/await on the nightly channel. This RFC,
however, is explicitly an **experimental RFC** and is not intended to be a
reference for stability. It is not intended that stackless coroutines will ever
become a stable feature of Rust without a further RFC. As coroutines are such a
large feature, however, testing the feature and gathering usage data needs to
happen on the nightly channel, meaning we need to land something in the
compiler!
This RFC is different from the previous [RFC 1823] and [RFC 1832] in that this
detailed design section will be mostly devoid of implementation details for
generators. This is intentionally done so to avoid bikeshedding about various
bits of syntax related to coroutines. While critical to stabilization of
coroutines these features are, as explained earlier, irrelevant to the "apparent
stability" of async/await and can be determined at a later date once we have
more experience with coroutines.
In other words, the intention of this RFC is to emphasize that point that **we
will focus on adding async/await through procedural macros and coroutines**. The
driving factor for stabilization is the real-world and high-impact use case of
async/await, and zero-cost futures will be an overall theme of the continued
work here.
It's worth briefly mentioning, however, some high-level design goals of the
concept of stackless coroutines:
* Coroutines should be compatible with libcore. That is, they should not require
any runtime support along the lines of allocations, intrinsics, etc.
* As a result, coroutines will roughly compile down to a state machine that's
advanced forward as its resumed. Whenever a coroutine yields it'll leave
itself in a state that can be later resumed from the yield statement.
* Coroutines should work similarly to closures in that they allow for capturing
variables and don't impose dynamic dispatch costs. Each coroutine will be
compiled separately (monomorphized) in the way that closures are today.
* Coroutines should also support some method of communicating arguments in and
out of itself. For example when yielding a coroutine should be able to yield a
value. Additionally when resuming a coroutine may wish to require a value is
passed in on resumption.
[RFC 1823]: https://github.com/rust-lang/rfcs/pull/1823
[RFC 1832]: https://github.com/rust-lang/rfcs/pull/1832
As a reference point @Zoxc has implemented generators in a [fork of
rustc][fork], and has been a critical stepping stone in experimenting with the
`#[async]` macro in the motivation section. This implementation may end up being
the original implementation of coroutines in the compiler, but if so it may
still change over time.
[fork]: https://github.com/Zoxc/rust/tree/gen
One important note is that we haven't had many experimental RFCs yet, so this
process is still relatively new to us! We hope that this RFC is lighter weight
and can go through the RFC process much more quickly as the ramifications of it
landing are much more minimal than a new stable language feature being added.
Despite this, however, there is also a desire to think early on about corner
cases that language features run into and plan for a sort of reference test
suite to exist ahead of time. Along those lines this RFC proposes a list of
tests accompanying any initial implementation of coroutines in the compiler,
covering. Finally this RFC also proposes a list of unanswered questions related
to coroutines which likely wish to be considered before stabilization
##### Open Questions - coroutines
* What is the precise syntax for coroutines?
* How are coroutines syntactically and functionally constructed?
* What do the traits related to coroutines look like?
* Is "coroutine" the best name?
* Are coroutines sufficient for implementing iterators?
* How do various traits like "the coroutine trait", the `Future` trait, and
`Iterator` all interact? Does coherence require "wrapper struct" instances to
exist?
##### Open Questions - async/await
* Is using a syntax extension too much considered to be creating a
"sub-language"? Does async/await usage feel natural in Rust?
* What precisely do you write in a signature of an async function? Do you
mention the future aspect?
* Can `Stream` implementations be created with similar syntax? Is async/await
with coroutines too specific to futures?
##### Tests - Basic usage
* Coroutines which don't yield at all and immediately return results
* Coroutines that yield once and then return a result
* Creating a coroutine which closes over a value, and then returning it
* Returning a captured value after one yield
* Destruction of a coroutine drops closed over variables
* Create a coroutine, don't run it, and drop it
* Coroutines are `Send` and `Sync` like closures are wrt captured variables
* Create a coroutine on one thread, run it on another
##### Tests - Basic compile failures
* Coroutines cannot close over data that is destroyed before the coroutine is
itself destroyed.
* Coroutines closing over non-`Send` data are not `Send`
##### Test - Interesting control flow
* Yield inside of a `for` loop a set number of times
* Yield on one branch of an `if` but not the other (take both branches here)
* Yield on one branch of an `if` inside of a `for` loop
* Yield inside of the condition expression of an `if`
##### Tests - Panic safety
* Panicking in a coroutine doesn't kill everything
* Resuming a panicked coroutine is memory safe
* Panicking drops local variables correctly
##### Tests - Debuginfo
* Inspecting variables before/after yield points works
* Breaking before/after yield points works
Suggestions for more test are always welcome!
# How We Teach This
[how-we-teach-this]: #how-we-teach-this
Coroutines are not, and will not become a stable language feature as a result of
this RFC. They are primarily designed to be used through async/await notation
and are otherwise transparent. As a result there are no specific plans at this
time for teaching coroutines in Rust. Such plans must be formulated, however,
prior to stabilization.
Nightly-only documentation will be available as part of the unstable book about
basic usage of coroutines and their abilities, but it likely won't be exhaustive
or the best learning for resource for coroutines yet.
# Drawbacks
[drawbacks]: #drawbacks
Coroutines are themselves a significant feature for the compiler. This in turns
brings with it maintenance burden if the feature doesn't pan out and can
otherwise be difficult to design around. It is thought, though, that coroutines
are highly likely to pan out successfully with futures and async/await notation
and are likely to be coalesced around as a stable compiler feature.
# Alternatives
[alternatives]: #alternatives
The alternatives to list here, as this is an experimental RFC, are more targeted
as alternatives to the motivation rather than the feature itself here. Along
those lines, you could imagine quite a few alternatives to the goal of tackling
the 2017 roadmap goal targeted in this RFC. There's quite a bit of discussion on
the [original rfc thread][rfc], but some highlight alternatives are:
* "Stackful coroutines" aka green threads. This strategy has, however, been
thoroughly explored in historical versions of Rust. Rust long ago had green
threads and libgreen, and consensus was later reached that it should be
removed. There are many tradeoffs with an approach like this, but it's safe to
say that we've definitely gained a lot of experimental and anecdotal evidence
historically!
* User-mode-scheduling is another possibility along the line of green threads.
Unfortunately this isn't implemented in all mainstream operating systems
(Linux/Mac/Windows) and as a result isn't a viable alternative at this time.
* ["Resumable expressions"][cpp] is a proposal in C++ which attempts to deal
with some of the "viral" concerns of async/await, but it's unclear how
applicable or easy it would apply to Rust.
[rfc]: https://github.com/rust-lang/rfcs/pull/2033
[cpp]: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2015/p0114r0.pdf
Overall while there are a number of alternatives, the most plausible ones have a
large amount of experimental and anecdotal evidence already (green
threads/stackful coroutines). The next-most-viable alternative (stackless
coroutines) we do not have much experience with. As a result it's believed that
it's time to explore and experiment with an alternative to M:N threading with
stackless coroutines, and continue to push on the 2017 roadmap goal.
Some more background about this motivation for exploring async/await vs
alternatives can also be found [in a comment on the RFC thread][comment].
[comment]: https://github.com/rust-lang/rfcs/pull/2033#issuecomment-309603972
# Unresolved questions
[unresolved]: #unresolved-questions
The precise semantics, timing, and procedure of an experimental RFC are still
somewhat up in the air. It may be unclear what questions need to be decided on
as part of an experimental RFC vs a "real RFC". We're hoping, though, that we
can smooth out this process as we go along!