Tutorial - State Machines
Let's again use the following async function as a reference point:
#![allow(unused)] fn main() { async fn greet(name: String) { println!("hello {name}"); tokio::time::sleep(Duration::from_secs(1)).await; println!("goodbye {name}"); } }
and let's just remind ourselves of what a state machine looks like (we shall ignore Pin for now)
#![allow(unused)] fn main() { trait Future { type Output; fn poll(&mut self, cx: &mut Context<'_>) -> Poll<Self::Output>; } }
Before we try to figure out what states we need to keep, let's consider the transitions instead.
The first transition is
#![allow(unused)] fn main() { println!("hello {name}"); tokio::time::sleep(Duration::from_secs(1)) }
The second transition is
#![allow(unused)] fn main() { println!("goodbye {name}"); }
Since we can pause at .await points, these have to be the places we save the states.
Then between the .awaits, including before the first and after the last, are our transition steps.
Since our async fn has only 1 await point, that will be our one intermediate state.
We will also have 2 more states to complement our transitions. One state for before the function runs, and one state for after the function is complete.
So let's see some code showing this off the states
#![allow(unused)] fn main() { enum GreetFut { Init { // Our initial state stores the argument of the function name: String, } Intermediate { // We must carry all values that are still in scope. name: String, // We must carry any intermediate futures that are still in progress sleep_fut: Sleep, } // When the future is done, there's no more values to store // as they have been dropped already. Done, } }
And now the transitions:
#![allow(unused)] fn main() { impl Future for GreetFut { // our function returns nothing type Output = (); fn poll(&mut self, cx: &mut Context<'_>) -> Poll<Self::Output> { loop { match *self { GreetFut::Init { name } => { // perform our first transition println!("hello {name}"); let sleep_fut = tokio::time::sleep(Duration::from_secs(1)); // update our state *self = GreetFut::Intermediate { name, sleep_fut }; // we need to try and continue the state machine continue; } GreetFut::Intermediate { name, mut sleep_fut } => { // check if we are ready to make the second transition ready!(sleep_fut.poll(cx)); // we are ready to perform our second transition println!("goodbye {name}"); // update our state again *self = GreetFut::Done; // since there are no more state, we should return ready return Poll::Ready(()); } // There is no state after done, so we cannot make any transition. GreetFut::Done => panic!("polled after completion"), } } } } }