Hot off the heels of RustFest Barcelona and the stabilization of async/.await
, I think it's safe to say that one of Rust's most anticipated language features has finally landed. And for that occasion (and because I've had some trouble understanding certain bits of it myself), I wanted to write a little introduction to asynchronous programming in Rust. We'll be creating a super simple application that fetches some data from the internet using the our newfound async
abilities. The stabilization of async/.await
also coincides nicely with another event I'm excited about: This week saw the release of the most recent entries in the mainline Pokémon games, Pokémon Sword and Shield, and because I'm a bit too busy to pick them up just yet, I'll make do by fetching data from the PokéAPI for now.
I'm assuming some base knowledge of Rust's syntax and ecosystem, but I hope that this is pretty accessible even to people very new to the community.
But before diving into the coding part, let's cover some basic concepts of asynchronous programming and how it might be a bit different in Rust than what you'd expect.
What does async
mean?
In Rust, when we talk about async
, we're talking about running code concurrently, or having multiple overlapping (in time) computations run on a single thread. Multithreading is a related, but distinct concept. Multithreading is ideal for when you've got computationally intensive tasks (so-called CPU-bound tasks) that can be spread across multiple, separated cores. Concurrent programming is better suited for when the task spends a lot of time waiting, such as for a response from a server. These tasks are called IO-bound.
So asynchronous programming lets us run multiple of these IO-bound computations at the same time on a single thread. They can run at the same time because when they're waiting for a response, they're just idle, so we can let the computer keep working on something that isn't waiting. When we reach a point where we need the result of an asynchronous computation, we must .await
it. In Rust, values that are 'awaitable' are known as 'futures'.
Rusty weirdness
async
in Rust may be a bit different from what you're used to in other languages. Having done asynchronous coding mostly in JavaScript and C#, it certainly was to me. Here's a few key things to understand:
An async
function does not (necessarily) start executing immediately
To start an asynchronous function, you must either .await
it or launch a task using an executor (we'll get to that in a moment). Until this happens, all you have is a Future
that has not started. Let's look at an example to make it clearer:
use async_std::task;
// ^ we need this for task spawning
async fn negate_async(n: i32) -> i32 {
println!("Negating {}", n);
task::sleep(std::time::Duration::from_secs(5)).await;
println!("Finished sleeping for {}!", n);
n * -1
}
async fn f() -> i32 {
let neg = negate_async(1);
// ... nothing happens yet
let neg_task = task::spawn(negate_async(2));
// ^ this task /is/ started
task::sleep(std::time::Duration::from_secs(1)).await;
// we sleep for effect.
neg.await + neg_task.await
// ^ this starts the first task `neg`
// and waits for both tasks to finish
}
So in the above little code snippet, here's what's going on.
- The first line imports
async_std::task
. There's more on this below, but we need an external library to run futures as the standard library does not come with an executor. - The async function
negate_async
takes as input a signed integer, sleeps for 5 seconds, and returns the negated version of that integer. - The async function
f
is more interesting: - The first line (
let neg ...
) creates aFuture
of thenegate_async
function and assigns it to theneg
variable. Importantly, it does /not/ start executing yet. - The next line of code (
let neg_task ...
) uses thetask::spawn
function to start executing theFuture
returned bynegate_async
. Like withneg
, theFuture
returned bynegate_async
is assigned to theneg_task
variable. - Next: we sleep for a second. This is so that it will be obvious from the output when a task starts running.
- Finally, we await both futures, add them together, and return them. By awaiting
neg
, we start executing theFuture
and run it to completion. Sinceneg_task
has already been started, we just wait for it to finish.
So what's the result of this, then?
Negating 2
# <- there's a 1 second pause here
Negating 1
Finished sleeping for 2!
Finished sleeping for 1!
As we can see, the second future, neg_task
, started executing as soon as it was called---thanks to task::spawn
---while neg
did not start executing until it was awaited.
You need an external library to use async/.await
As was briefly alluded to above, you need to reach for an external library to do asynchronous programming in Rust. This took me a while to understand, as I'm used to it being part of the language experience. In Rust, however, *you need a dedicated executor/*[fn:1]. The executor is what takes care of /executing the futures, polling them and returning the results when they're done. The standard library does not come with an executor, so we need to reach out to an external crate for this. There are a few ones to choose from, but the two most prominent ones are ~async-std~ (which we're using here) and ~tokio~.
A minimal async example!
Alright, let's get practical. This is the reason that I'm writing this post. As mentioned at the start, we'll be creating a super simple application that fetches some Pokémon data and prints it to the console. For preparation, make sure you've got at least version 1.39 of Rust and cargo available.
Step 1: creating the application
Let's create a new application! Simply run this command in your preferred directory:
cargo new async-basics
Step 2: Dependencies
We're going to be using ~async-std~ for spawning tasks, and ~surf~ to fetch data from the API. Let's add them to the Cargo.toml file. Your whole file should look something like this:
[package]
name = "async-basics"
version = "0.1.0"
authors = ["Your Name <your.email@provider.tld>"]
edition = "2018"
[dependencies]
async-std = "1"
surf = "1"
Nice! This is going swimmingly!
Step 3: Fetch data
Okay, final step. Let's modify the main.rs
file. We'll make it as simple as possible. Here's what we want to use:
use async_std::task;
use surf;
// fetch data from a url and return the results as a string.
// if an error occurs, return the error.
async fn fetch(url: &str) -> Result<String, surf::Exception> {
surf::get(url).recv_string().await
}
// execute the fetch function and print the results
async fn execute() {
match fetch("https://pokeapi.co/api/v2/move/surf").await {
Ok(s) => println!("Fetched results: {:#?}", s),
Err(e) => println!("Got an error: {:?}", e),
};
}
fn main() {
task::block_on(execute());
// ^ start the future and wait for it to finish
}
That's all the code you need. In fact, it's more than what you need, as some parts have been broken up for legibility. Let's walk through it!
- ~use~ statements
- Nothing exciting here. Just importing the crates we declared in the Cargo.toml file:
surf
andasync_std
. - ~fetch~
- This is simply a thin wrapper around the
surf::get
function which returns either the payload as aString
or anException
if something went wrong. - ~execute~
- This function calls fetch with the endpoint for the move
Surf
, waits for the result to return, and then matches on the result. If everything went well: print the output. Else: print the error. - ~main~
main
simply kicks offexecute
and waits for it to finish.task::block_on
is a synchronous counterpart totask::spawn
that starts an asynchronous operation, but blocks until it has finished. Because themain
function can't itself beasync
(at least not at the time of writing), we can't use.await
in it, but we can block on asynchronous operations.
Step 4: Extend it!
Hey, you made it this far; congrats! That's all I really have in store for you for this one, but if you want to play around a bit more, how about adding ~serde~ and try using surf
's recv_json<T>
instead? If you'd rather keep looking at async/.await
, how about performing multiple requests simultaneously? Or how about making a PokéAPI CLI? (Ooh, that sounds like fun! Hit me up if you're doing this; I want in!)
Parting words and resources
So there you have it, dear reader. I hope you have found this useful. async/.await
is finally stabilized and it feels like we've taken a major leap forward. I'm very much looking forward to seeing what happens in the coming months and what the community makes of this.
If you're looking for more resources on async Rust, be sure to check out the Async Book. I also recommend the async-std book for some extra insights.
Until next time: take care!
Footnotes
See this insightful Reddit comment thread for more on this.