How to write async functions in Rust
Understanding the terminologies involved.
future: A rust trait that can be implemented by functions. The functionality it adds is that when a function which implements this trait is blocked, it will yield control of the OS thread it is running on.
If you don't know what a trait is -> https://usethesource.hashnode.dev/rust-traits-vs-java-interfaces
async: Transforms a block of code into a state machine that implements a trait called future.
.await: Can be used to wait for a specific function to complete where the function implements the future trait.
join!: Similar to .await but can be used on multiple functions that implement the future trait.
block_on(some_function()): Blocking call on some_function()
Dependencies
[dependencies]
futures = "0.3"
Why use async?
Let's try to understand this through the following example.
Let's say we have the following 3 tasks:
- read_book
- get_eggs
- make_omelette
Here, the task read_book is independent of the other tasks whereas make_omelette depends on get_eggs.
Now, suppose we were to execute this in a synchronous manner.
Scenario 1
read_book()
get_eggs()
make_omelette()
Suppose read_book fires an API call to a server that just crashed.
This will result in all the make_omelette() getting delayed.
Scenario 2
get_eggs()
make_omelette()
read_book()
Suppose get_eggs tries to get the eggs from a database that for some reason is experiencing downtime.
This will result in all the read_book() getting delayed.
Writing these functions in an asynchronous manner will allow us to avoid the pitfalls that come with synchronous programming.
It is important to understand we are not talking about running these tasks on different threads. This will work even in a single-threaded environment.
Implementing this in Rust.
use futures::executor::block_on;
// Independent task.
// `async` makes this implement `Future` trait.
async fn read_book() {
println!("Book read.");
}
// Dependent on `get_eggs` for the number of eggs.
// `async` makes this implement `Future` trait.
async fn make_omelette(num_eggs: &str) {
println!("Making omelette with: {}", num_eggs)
}
// Independent task.
// Gives back the number of eggs to use.
// This information will be used by the make_omelette() function.
// `async` makes this implement `Future` trait.
async fn get_eggs() -> &'static str{
println!("Getting Eggs.");
return "2 eggs"
}
// Independent task.
// Internally calles 2 async tasks.
// `async` makes this implement `Future` trait.
async fn get_eggs_and_make_omelette() {
// Wait till you get eggs before making omelette.
// We use `.await` here rather than `block_on` to prevent blocking the thread.
// (program is running on a single threaded).
let num_eggs = get_eggs().await;
make_omelette(num_eggs).await;
}
// `async` makes this implement `Future` trait.
// Fires all the tasks.
async fn perform_tasks() {
let f1 = get_eggs_and_make_omelette();
let f2 = read_book();
// If `get_eggs_and_make_omelette` becomes blocked,
// the `read_book` future will take over the current thread.
// If `read_book` becomes blocked,
// `get_eggs_and_make_omelette` can take back over.
// If both futures are blocked,
// then `perform_tasks` is blocked and will yield to the executor
// since it is itself an async function.
futures::join!(f1, f2);
}
fn main() {
// Blocking call on the async_perform_tasks.
block_on(perform_tasks());
}
I've purposely left the explanation in the comments because it is a much better way to understand what each line of code is doing. (I didn't want you to keep scrolling between the code and explanation)




