Async

Rust supports asynchronous execution in single-threaded and multi-threaded scenarios. With single threads, multiple tasks can run concurrently (not parallel).

In order for async functions to make progress, the poll() function must be called. When you call an async Rust function, you aren't actually executing the code inside the function, because Rust futures are "lazy." Instead, an async function returns something known as a Future, wrapping the return (Output) type of the function.

In order for a future to execute, an "executor" must periodically call the Future's poll() function. Once a Future has completed, it has no more work to do, and the executor can stop invoking the poll() function.

Implement Future Trait

You can create your own asynchronous tasks by simply implementing the std::future::Future trait on custom types.

  • Set the Output type of the future (aka. the future's "return type")
  • Define behavior of the poll() function

You might be wondering how exactly the poll() function should be implemented. For starters, you'll want to think about how to break up your "work" into smaller chunks, that can be progressively completed.

Example: Implement Future Trait

Let's explore an example of implementing the Future trait. We will build a Rust binary that represents a Horse 🐴 in a race.

In this example, we will implement a custom type representing a Horse. The Horse will run a race, and perform several laps. Each lap that the horse runs will be a single "unit of work." When we call the poll() function on the Horse, we will advance the number of completed laps by one.

Install Tokio Async Executor

In order to execute our custom future, we'll need to install an async executor.

cargo add tokio --features=full

Turn your main() function into an async function by using the following snippet.

#[tokio::main]
async fn main() {
}

Define Horse Struct

The first thing we need to do is define the Horse struct itself. We'll add three data fields to the struct, to represent the following pieces of information:

  • name: The horse's name
  • laps_total: The number of race laps that the horse is planned to completed
  • laps_current: The number of race laps that the horse has completed thus far
#![allow(unused)]
fn main() {
struct Horse {
  laps_total: u8,
  laps_current: u8,
  name: String,
}
}

Now that the Horse has been defined, we need to instantiate it in our main() function.

#[tokio::main]
async fn main() {
  let horse01 = Horse{ laps_total: 5, laps_current: 0, name: "Dakota".to_string() };
  let horse02 = Horse{ laps_total: 3, laps_current: 0, name: "Shadowfax".to_string() };
}

To execute a future, we need to await it. The await keyword is used like a function call, but without parentheses. If you try to await a struct instance that doesn't implement the Future trait, then you'll receive an error similar to the one below.

let result = horse01.await;

Horse is not a future the trait Future is not implemented for Horse Horse must be a future or must implement IntoFuture to be awaited required for Horse to implement IntoFuture

Let's proceed by taking the compiler's recommendation, and implement the std::future::Future trait on our Horse struct. We'll come back to await when we're done implementing Future.

Implement Future Trait

Defining the Output type for the Future is easy. Simply use the type keyword to denote the type that will be returned by the Future's Output. You can also think of this as the Future's "return type."

You don't always need to return an output from the Future, so you can stick with the "unit" type.

#![allow(unused)]
fn main() {
impl std::future::Future for Horse {
  type Output = (); // Declare the output type resulting from the completed Future
}
}

Implementing the poll() method is the more complicated bit, that we need to address.

#![allow(unused)]
fn main() {
impl std::future::Future for Horse {
  type Output = ();
  
  // Implement the function to make "progress" on the Future
  fn poll(self: std::pin::Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> std::task::Poll<Self::Output> {    
  }
}
}

When you add the poll() function definition, without defining the function body, you'll receive a compiler error similar to the following:

mismatched types expected enum Poll<Task> found unit type ()

This message appears because your poll() function needs to return a variant of std::task::Poll. This Poll enum has only two variants:

  • Pending
  • Ready(T)

If your custom Future still has more work to do, then you need to return Poll::Pending. Otherwise, if your Future is complete, and you are ready to return the final result, use Poll::Ready(T).

If the Horse's number of completed laps is less than the target number of laps, then we need to run a lap (do some work), and then return Poll::Pending. Let's implement that now. The poll() function allows you to reference and mutate the future instance using the self argument.

Also notice that we return Ready(()) once the Horse has completed running all the laps. All code paths must return a value, so if you don't include that line, you'll receive the error below.

if may be missing an else clause ^ expected Poll<()>, found ()

#![allow(unused)]
fn main() {
impl std::future::Future for Horse {
  type Output = ();

  // Implement the function to make "progress" on the Future
  fn poll(self: std::pin::Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> std::task::Poll<Self::Output> {    
    if self.laps_current < self.laps_total {
      println!("Running another lap!")   // Provide some debugging output, so we can see what is happening.
      self.get_mut().laps_current += 1;  // After the race lap has completed, add one to laps_current.
      return std::task::Poll::Pending;   // Return "Pending" to indicate that there's still more work to do.
                                         // By returning "Pending", we politely give control back to the executor.
                                         // The executor will (hopefully) call poll() on our Future again.
    }
    return std::task::Poll::Ready(());
  }
}
}

If you run this future as-is, then your program will hang. That's because you also have to "wake up" your poll() function, using the waker, accessed via the Context argument. If you don't wake up your Future, then it will indefinitely stay in the Pending state.

As an example, the following code snippet should hang.

#[tokio::main]
async fn main() {
  let horse01 = Horse{ laps_total: 5, laps_current: 0, name: "Dakota".to_string() };
  let result = horse01.await;
}

To wake up the Future, let's add the following line, right before the line where we return Pending.

#![allow(unused)]
fn main() {
cx.waker().wake_by_ref();
}

Now, run your application with cargo run and you should see it run to completion.