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 namelaps_total
: The number of race laps that the horse is planned to completedlaps_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 traitFuture
is not implemented forHorse
Horse must be a future or must implementIntoFuture
to be awaited required forHorse
to implementIntoFuture
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 anelse
clause ^ expectedPoll<()>
, 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.