RustArena

Step 2 of 7

The runtime, and sleeping without blocking

#[tokio::main] hides a few lines. Written out by hand, it's this:

use std::time::Duration;

async fn boil_water() -> &'static str {
    tokio::time::sleep(Duration::from_millis(5)).await;
    "water boiled"
}

fn main() {
    let runtime = tokio::runtime::Runtime::new().unwrap();
    let result = runtime.block_on(boil_water());
    println!("{result}");
}
water boiled

tokio::time::sleep, not std::thread::sleep

tokio::time::sleep(duration).await pauses the current task. The worker thread is free to run other tasks in the meantime, and the timer wakes the task up later.

std::thread::sleep pauses the whole thread. In async code, that freezes every task scheduled on that worker. The program still produces the right output, which is what makes it dangerous: it's just slow, and under load everything stalls. The golden rule of async code: never block. No std::thread::sleep, no long CPU loops, no blocking file or network calls inside async functions. Use the async versions (tokio::time::sleep, tokio::fs, tokio::net), or move the blocking work elsewhere (step 7).

Awaits run one after another

Each .await waits for its future to finish before the next line runs:

use std::time::Duration;
use tokio::time::sleep;

async fn fetch_user(id: u32) -> String {
    sleep(Duration::from_millis(20)).await; // pretend this is a network call
    format!("user{id}")
}

#[tokio::main]
async fn main() {
    let a = fetch_user(1).await;
    let b = fetch_user(2).await;
    println!("{a} and {b}");
}
user1 and user2

This takes about 40 ms: the second call doesn't start until the first is done. That's right when the second call needs the first one's result. When they're independent, you can run them at the same time: next step.

Common mistakes

Your task

Services fail sometimes, and real clients retry with backoff: wait a little longer after each failure. Write async fn ping_with_retry(service: &mut FlakyService, max_tries: u32) -> Result<String, String>. Try service.ping().await up to max_tries times (assume at least 1):

Challenge 2

Call async code from a normal fn main, without #[tokio::main]: fill in the function that creates the runtime and the method that runs a future to completion. The program should print cache ready with 128 entries.

Challenge 3

This kitchen timer prints the right lines, but std::thread::sleep blocks the runtime's worker thread while it waits. Switch timer to Tokio's non-blocking sleep (and await it). The output stays the same:

eggs: done after 3 ms
toast: done after 2 ms
tea: done after 4 ms

Previous: Futures, async fn and .await Next: Running futures at the same time: join! and try_join!