-
Notifications
You must be signed in to change notification settings - Fork 1.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add a schematic state machine implementing Future #2048
base: main
Are you sure you want to change the base?
Changes from 4 commits
9a59515
1905367
c3879ce
3102d11
e2884fe
ff0959b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
--- | ||
minutes: 7 | ||
--- | ||
|
||
# State Machine | ||
|
||
Rust transforms an async function or block to a hidden type that implements | ||
`Future`, using a state machine to track the function's progress. The details of | ||
this transform are complex, but it helps to have a schematic understanding of | ||
what is happening. | ||
|
||
```rust,editable,compile_fail | ||
use futures::executor::block_on; | ||
use std::future::Future; | ||
use std::pin::Pin; | ||
use std::task::{Context, Poll}; | ||
|
||
async fn send(s: &str) { | ||
println!("{s}"); | ||
} | ||
|
||
/* | ||
async fn count_to(count: i32) { | ||
for i in 1..=count { | ||
send("tick").await; | ||
} | ||
} | ||
*/ | ||
|
||
fn count_to(count: i32) -> CountToFuture { | ||
CountToFuture { state: CountToState::Init, count, i: 0 } | ||
} | ||
|
||
struct CountToFuture { | ||
state: CountToState, | ||
count: i32, | ||
i: i32, | ||
} | ||
|
||
enum CountToState { | ||
Init, | ||
Sending(Pin<Box<dyn Future<Output = ()>>>), | ||
} | ||
|
||
impl std::future::Future for CountToFuture { | ||
type Output = (); | ||
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> { | ||
loop { | ||
match &mut self.state { | ||
CountToState::Init => { | ||
self.i = 1; | ||
self.state = CountToState::Sending(Box::pin(send("tick"))); | ||
} | ||
CountToState::Sending(send_future) => { | ||
match send_future.as_mut().poll(cx) { | ||
Poll::Pending => return Poll::Pending, | ||
Poll::Ready(_) => { | ||
self.i += 1; | ||
if self.i > self.count { | ||
return Poll::Ready(()); | ||
} else { | ||
self.state = | ||
CountToState::Sending(Box::pin(send("tick"))); | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
|
||
fn main() { | ||
block_on(count_to(5)); | ||
} | ||
``` | ||
|
||
<details> | ||
|
||
While this code will run, it is simplified from what the real state machine | ||
would do. The important things to notice here are: | ||
|
||
- Calling an async function does nothing but construct a value, ready to start | ||
on the first call to `poll`. | ||
- All local variables are stored in the function's future struct, including an | ||
enum to identify where execution is currently suspended. The real generated | ||
state machine would not initialize `i` to 0. | ||
- An `.await` in the async function is translated into a call to that async | ||
function, then polling the future it returns until it is `Poll::Ready`. The | ||
real generated state machine would contain the future type defined by `send`, | ||
but that cannot be expressed in Rust syntax. | ||
- Execution continues eagerly until there's some reason to block. Try returning | ||
`Poll::Pending` in the `CountToState::Init` branch of the match, in hopes that | ||
`poll` will be called again with state `CountToState::Sending`. `block_on` | ||
will not do so! | ||
|
||
</details> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
--- | ||
minutes: 3 | ||
--- | ||
|
||
# Recursion | ||
|
||
An async function's future type _contains_ the futures for all functions it | ||
calls. This means a recursive async functions are not allowed. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would clarify this by invoking the analogy with recursive enums: they need an indirection to avoid being infinite-sized types. Prior to Rust 1.77, recursion in |
||
|
||
```rust,editable,compile_fail | ||
use futures::executor::block_on; | ||
|
||
async fn count_to(n: u32) { | ||
if n > 0 { | ||
count_to(n - 1).await; | ||
println!("{n}"); | ||
} | ||
} | ||
|
||
fn main() { | ||
block_on(count_to(5)); | ||
} | ||
``` | ||
|
||
<details> | ||
|
||
This is a quick illustration of how understanding the state machine helps to | ||
understand errors. Recursion would require `CountToFuture` to contain a field of | ||
type `CountToFuture`, which is impossible. | ||
|
||
Fix this with `Box::pin(count_to(n-1)).await;`, boxing the future returned from | ||
`count_to`. | ||
|
||
</details> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it makes more sense for explanation to first trace the control flow so we can enumerate states and get in our heads something like "we'll have three possible execution states for this async fn, so it makes sense that its future type would be (morally) an enum with three variants". Then we can look at variable liveness at each of the awaits to determine the payload of each variant. This lets us get to "futures store live locals" without having to introduce the notion of liveness explicitly like a compilers course would--these are just the variables whose values we'll need to run the rest of the function.
I think it's better for an example of the state machine transform to use an async fn that doesn't have async in a loop, so that it's easy for readers to enumerate the entire set of states in their head.
So I might suggest an example more like this:
This gives us three states:
x
double_x
until we return from our firstawait
bytes_written
until we return from our secondawait
This is, I think, less confusing than a state we return to across iterations of our loop on
1..=count
.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Implicit in this suggestion is to use enum variants to represent the liveness of variables. That's great, but does require a bit more mucking about with pins than we want to present at this point. In fact, it uses
pin_project
, which we don't even talk about in the Pin section. Should I maybe revert this to a flat struct with afut
field, just to avoid this?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think it's so important to make this example have perfect fidelity or be compilable/runnable--pseudocode to capture the transformation would be fine, and would allow us to side-step
Pin
questions. The sub-future, if we really want to represent it, could just be astd::future::Ready
or isomorphic. The most significant thing is not the state of the child future but the fact that at await points we capture live state.