From 9a59515b8611eb0ff8b1dd7d3848cd9546e08a29 Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Fri, 3 May 2024 18:26:49 +0000 Subject: [PATCH 1/6] Add a schematic state machine implementing Future --- src/SUMMARY.md | 1 + src/concurrency/async-pitfalls/pin.md | 11 ++- src/concurrency/async/state-machine.md | 96 ++++++++++++++++++++++++++ 3 files changed, 101 insertions(+), 7 deletions(-) create mode 100644 src/concurrency/async/state-machine.md diff --git a/src/SUMMARY.md b/src/SUMMARY.md index 1e4be22f6301..672e33604f92 100644 --- a/src/SUMMARY.md +++ b/src/SUMMARY.md @@ -382,6 +382,7 @@ - [Async Basics](concurrency/async.md) - [`async`/`await`](concurrency/async/async-await.md) - [Futures](concurrency/async/futures.md) + - [State Machine](concurrency/async/state-machine.md) - [Runtimes](concurrency/async/runtimes.md) - [Tokio](concurrency/async/runtimes/tokio.md) - [Tasks](concurrency/async/tasks.md) diff --git a/src/concurrency/async-pitfalls/pin.md b/src/concurrency/async-pitfalls/pin.md index fc764a8af083..bcfbe4256661 100644 --- a/src/concurrency/async-pitfalls/pin.md +++ b/src/concurrency/async-pitfalls/pin.md @@ -4,13 +4,10 @@ minutes: 20 # `Pin` -Async blocks and functions return types implementing the `Future` trait. The -type returned is the result of a compiler transformation which turns local -variables into data stored inside the future. - -Some of those variables can hold pointers to other local variables. Because of -that, the future should never be moved to a different memory location, as it -would invalidate those pointers. +Recall an async function or block creates a type implementing `Future` and +containing all of the local variables. Some of those variables can hold +references (pointers) to other local variables. To ensure those remain valid, +the future can never be moved to a different memory location. To prevent moving the future type in memory, it can only be polled through a pinned pointer. `Pin` is a wrapper around a reference that disallows all diff --git a/src/concurrency/async/state-machine.md b/src/concurrency/async/state-machine.md new file mode 100644 index 000000000000..4d45cf3ce7b3 --- /dev/null +++ b/src/concurrency/async/state-machine.md @@ -0,0 +1,96 @@ +--- +minutes: 5 +--- + +# 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 +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>>), +} + +impl std::future::Future for CountToFuture { + type Output = (); + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + 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)); +} +``` + +
+ +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 exection 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 not box this future. +- 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! + +
From 1905367dbc12af216411cc880c503f9c4f50eedf Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Fri, 3 May 2024 18:41:08 +0000 Subject: [PATCH 2/6] add recursion subslide --- src/SUMMARY.md | 1 + src/concurrency/async/state-machine.md | 5 +-- .../async/state-machine/recursion.md | 34 +++++++++++++++++++ 3 files changed, 38 insertions(+), 2 deletions(-) create mode 100644 src/concurrency/async/state-machine/recursion.md diff --git a/src/SUMMARY.md b/src/SUMMARY.md index 672e33604f92..1507362dcf24 100644 --- a/src/SUMMARY.md +++ b/src/SUMMARY.md @@ -383,6 +383,7 @@ - [`async`/`await`](concurrency/async/async-await.md) - [Futures](concurrency/async/futures.md) - [State Machine](concurrency/async/state-machine.md) + - [Recursion](concurrency/async/state-machine/recursion.md) - [Runtimes](concurrency/async/runtimes.md) - [Tokio](concurrency/async/runtimes/tokio.md) - [Tasks](concurrency/async/tasks.md) diff --git a/src/concurrency/async/state-machine.md b/src/concurrency/async/state-machine.md index 4d45cf3ce7b3..323216995d34 100644 --- a/src/concurrency/async/state-machine.md +++ b/src/concurrency/async/state-machine.md @@ -1,5 +1,5 @@ --- -minutes: 5 +minutes: 7 --- # State Machine @@ -87,7 +87,8 @@ would do. The important things to notice here are: 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 not box this future. + 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` diff --git a/src/concurrency/async/state-machine/recursion.md b/src/concurrency/async/state-machine/recursion.md new file mode 100644 index 000000000000..91935c7302aa --- /dev/null +++ b/src/concurrency/async/state-machine/recursion.md @@ -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. + +```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)); +} +``` + +
+ +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`. + +
From c3879cea045a637984bc4281d7a119697f17ec5c Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Fri, 3 May 2024 18:52:45 +0000 Subject: [PATCH 3/6] typo --- src/concurrency/async/state-machine.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/concurrency/async/state-machine.md b/src/concurrency/async/state-machine.md index 323216995d34..ea1b1b21867e 100644 --- a/src/concurrency/async/state-machine.md +++ b/src/concurrency/async/state-machine.md @@ -83,7 +83,7 @@ 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 exection is currently suspended. The real generated + 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 From 3102d11c8c344dc4bd601c8b517c24b0396a0476 Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Fri, 3 May 2024 19:02:03 +0000 Subject: [PATCH 4/6] compile_fail due to futures missinng in mdbook test --- src/concurrency/async/state-machine.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/concurrency/async/state-machine.md b/src/concurrency/async/state-machine.md index ea1b1b21867e..ddf23214d044 100644 --- a/src/concurrency/async/state-machine.md +++ b/src/concurrency/async/state-machine.md @@ -9,7 +9,7 @@ Rust transforms an async function or block to a hidden type that implements this transform are complex, but it helps to have a schematic understanding of what is happening. -```rust,editable +```rust,editable,compile_fail use futures::executor::block_on; use std::future::Future; use std::pin::Pin; From e2884fe24c344a81072023592b34d4468816c2e3 Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Mon, 6 May 2024 18:23:03 +0000 Subject: [PATCH 5/6] recursion review notes --- src/concurrency/async/state-machine/recursion.md | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/concurrency/async/state-machine/recursion.md b/src/concurrency/async/state-machine/recursion.md index 91935c7302aa..0fafc3287754 100644 --- a/src/concurrency/async/state-machine/recursion.md +++ b/src/concurrency/async/state-machine/recursion.md @@ -26,9 +26,18 @@ fn main() { 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. +type `CountToFuture`, which is impossible. Compare to the common Rust error of +building an `enum` that contains itself, such as + +```rust +enum BinTree { + Node { value: T, left: BinTree, right: BinTree }, + Nil, +} +``` Fix this with `Box::pin(count_to(n-1)).await;`, boxing the future returned from -`count_to`. +`count_to`. This only became possible recently (Rust 1.77.0), before which all +recursion was prohibited. From ff0959bbf63175af6f2fc2fc43bc5aea25482c0c Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Mon, 6 May 2024 18:55:20 +0000 Subject: [PATCH 6/6] simpler example fn, use pin_project --- src/concurrency/async/state-machine.md | 87 +++++++++++++++----------- 1 file changed, 51 insertions(+), 36 deletions(-) diff --git a/src/concurrency/async/state-machine.md b/src/concurrency/async/state-machine.md index ddf23214d044..3ec1ecc9442d 100644 --- a/src/concurrency/async/state-machine.md +++ b/src/concurrency/async/state-machine.md @@ -11,57 +11,72 @@ what is happening. ```rust,editable,compile_fail use futures::executor::block_on; +use pin_project::pin_project; use std::future::Future; use std::pin::Pin; use std::task::{Context, Poll}; -async fn send(s: &str) { - println!("{s}"); +async fn send(s: String) -> usize { + println!("{}", s); + s.len() } /* -async fn count_to(count: i32) { - for i in 1..=count { - send("tick").await; - } +async fn example(x: i32) -> usize { + let double_x = x*2; + let mut count = send(format!("x = {x}")).await; + count += send(format!("double_x = {double_x}")).await; + count } */ -fn count_to(count: i32) -> CountToFuture { - CountToFuture { state: CountToState::Init, count, i: 0 } -} - -struct CountToFuture { - state: CountToState, - count: i32, - i: i32, +fn example(x: i32) -> ExampleFuture { + ExampleFuture::Init { x } } -enum CountToState { - Init, - Sending(Pin>>), +#[pin_project(project=ExampleFutureProjected)] +enum ExampleFuture { + Init { + x: i32, + }, + FirstSend { + double_x: i32, + #[pin] + fut: Pin>>, + }, + SecondSend { + count: usize, + #[pin] + fut: Pin>>, + }, } -impl std::future::Future for CountToFuture { - type Output = (); +impl std::future::Future for ExampleFuture { + type Output = usize; fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { loop { - match &mut self.state { - CountToState::Init => { - self.i = 1; - self.state = CountToState::Sending(Box::pin(send("tick"))); + match self.as_mut().project() { + ExampleFutureProjected::Init { x } => { + let double_x = *x * 2; + let fut = Box::pin(send(format!("x = {x}"))); + *self = ExampleFuture::FirstSend { double_x, fut }; + } + ExampleFutureProjected::FirstSend { double_x, mut fut } => { + match fut.as_mut().poll(cx) { + Poll::Pending => return Poll::Pending, + Poll::Ready(count) => { + let fut = + Box::pin(send(format!("double_x = {double_x}"))); + *self = ExampleFuture::SecondSend { count, fut }; + } + } } - CountToState::Sending(send_future) => { - match send_future.as_mut().poll(cx) { + ExampleFutureProjected::SecondSend { count, mut fut } => { + match fut.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"))); - } + Poll::Ready(tmp) => { + *count += tmp; + return Poll::Ready(*count); } } } @@ -71,7 +86,7 @@ impl std::future::Future for CountToFuture { } fn main() { - block_on(count_to(5)); + println!("result: {}", block_on(example(5))); } ``` @@ -90,8 +105,8 @@ would do. The important things to notice here are: 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` + `Poll::Pending` in the `ExampleState::Init` branch of the match, in hopes that + `poll` will be called again with state `ExampleState::Sending`. `block_on` will not do so!