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
Runtime::new()starts Tokio: a pool of worker threads (one per CPU core by default) plus a timer and an I/O driver.block_on(future)runs a future to completion and blocks the current (normal, non-async) thread until it's done. It's the bridge from ordinary code into async code. You'll use it in tests, and in small tools that call one async library.- Inside the runtime, work is organized in tasks: lightweight jobs that the runtime switches between whenever one of them is waiting at an
.await.main's future is the first task. #[tokio::main(flavor = "current_thread")]runs everything on one thread, handy for small tools and tests.
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
std::thread::sleepin async code: correct output, frozen worker thread. Usetokio::time::sleep(...).await.- Forgetting
.awaitonsleep:tokio::time::sleep(d);creates a timer future and drops it, so nothing waits (and rustc warns:unused `Sleep` that must be used). - Calling
block_oninside async code: Tokio panics with "Cannot start a runtime from within a runtime".block_onis only for the boundary from normal code; inside async code, just.await.
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):
- on success at attempt
n(counting from 1), returnOk(format!("{reply} (attempt {n})")); - after a failure, if there are tries left, wait
2^nmilliseconds with Tokio's sleep (Duration::from_millis(2u64.pow(n))) before trying again; - if every try fails, return
Err(format!("gave up after {max_tries} attempts: {last_error}"))with the last error message.
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!