RustArena

Step 1 of 7

Futures, async fn and .await

Threads are great for keeping CPU cores busy. But a lot of real work is waiting: for a database query, a web API, a file, a timer. A web server with 10,000 open connections spends almost all of its time waiting for the network. One OS thread per connection means 10,000 threads, each with its own stack, and the operating system constantly switching between them.

Async code lets a few threads juggle thousands of waiting jobs. Whenever a job would wait, it steps aside, and the thread runs another job that's ready to make progress. Two keywords make it work:

async fn fetch_price(item: &str) -> u32 {
    match item {
        "kelp" => 3,
        "pearl" => 250,
        _ => 0,
    }
}

#[tokio::main]
async fn main() {
    let price = fetch_price("pearl").await;
    println!("pearl costs {price}");
    let total = fetch_price("kelp").await * 4;
    println!("four kelp cost {total}");
}
pearl costs 250
four kelp cost 12

Futures are lazy

Calling an async function does nothing until the future is awaited:

async fn load_profile(name: &str) -> String {
    println!("loading {name}...");
    format!("{name} (level 12)")
}

#[tokio::main]
async fn main() {
    let future = load_profile("ferris");
    println!("future created, nothing loaded yet");
    let profile = future.await;
    println!("got {profile}");
}
future created, nothing loaded yet
loading ferris...
got ferris (level 12)

Forget the .await and the work never happens. The compiler warns you: warning: unused implementer of `Future` that must be used, with the note futures do nothing unless you .await or poll them. That warning almost always means a bug.

The runtime

Rust itself gives you async, .await and the Future trait (in the standard library), but not the machinery that runs futures, the runtime (also called an executor). You pick one as a crate. Tokio is by far the most widely used, and most async crates (the axum web framework, the reqwest HTTP client, the sqlx database library) are built on it:

[dependencies]
tokio = { version = "1", features = ["full"] }

#[tokio::main] turns async fn main into a normal main that starts a Tokio runtime and runs your code on it. Without it, rustc stops you with error[E0752]: `main` function is not allowed to be `async` .

.await only works in async code

async fn price() -> u32 {
    5
}

fn main() {
    let p = price().await;
    println!("{p}");
}
error[E0728]: `await` is only allowed inside `async` functions and blocks

The fix is to make the calling function async too, and its callers, all the way up to main. That's normal: async code spreads up the call chain.

Common mistakes

Your task

Fill in the three blanks: the attribute that starts Tokio, the keyword that makes main asynchronous, and the keyword that waits for the first future. The program should print in stock: 10 items.

Challenge 2

double_score is awaited in main, so it has to be an async function. Fill in the keyword. The program should print score: 42.

Challenge 3

Write two async functions and use them from main:

In main, print the best score of ana, bo and cy:

ana: Some(50)
bo: Some(80)
cy: None

Next: The runtime, and sleeping without blocking