RustArena

Step 4 of 7

Tasks: tokio::spawn and JoinHandle

join! runs futures inside one task. tokio::spawn(future) starts a new, independent task. The runtime schedules tasks on its worker threads, so spawned tasks can also run in parallel on several cores. It returns a tokio::task::JoinHandle<T>, and awaiting the handle gives Result<T, JoinError>: Err if the task panicked or was cancelled.

Tasks are cheap: a few hundred bytes each, against megabytes of stack for a thread. A server running 100,000 tasks, one per connection, is perfectly normal.

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

async fn check_server(name: String, latency_ms: u64) -> String {
    sleep(Duration::from_millis(latency_ms)).await;
    format!("{name}: up ({latency_ms} ms)")
}

#[tokio::main]
async fn main() {
    let servers = vec![("eu-1", 15), ("us-1", 5), ("ap-1", 10)];
    let mut handles = Vec::new();
    for (name, ms) in servers {
        handles.push(tokio::spawn(check_server(name.to_string(), ms)));
    }
    for handle in handles {
        println!("{}", handle.await.unwrap());
    }
}
eu-1: up (15 ms)
us-1: up (5 ms)
ap-1: up (10 ms)

The same pattern as threads in module 31: spawn everything, keep the handles, await them in spawn order. us-1 finishes first, but the results print in a fixed order.

'static and Send, again

tokio::spawn has the same requirements as thread::spawn, for the same reasons:

use std::rc::Rc;
use std::time::Duration;

#[tokio::main]
async fn main() {
    let handle = tokio::spawn(async {
        let name = Rc::new(String::from("crab"));
        tokio::time::sleep(Duration::from_millis(1)).await;
        name.len()
    });
    println!("{}", handle.await.unwrap());
}
error: future cannot be sent between threads safely

The full message points at the .await and says name is still in use after it. Use Arc, or make sure the value is dropped before the .await.

Panics and cancellation

A panic inside a task doesn't bring down the program: the task ends, and its handle's .await returns an Err(JoinError) (e.is_panic() is true). handle.abort() cancels a task; awaiting it then gives an Err where is_cancelled() is true. And when main returns, the runtime shuts down and drops every task that hasn't finished, so await the handles you care about.

Many tasks: JoinSet

tokio::task::JoinSet holds a group of tasks: set.spawn(future) adds one, and set.join_all().await waits for all of them and returns their outputs in a Vec, in the order they finished (sort before printing!). It's handy when you don't need each handle separately.

Common mistakes

Your task

Write async fn fetch_all(ids: Vec<u32>) -> Vec<String>. Spawn one task per id with tokio::spawn, each running fetch_user(id), then await the handles and return the results in the same order as ids. One hidden test fetches 300 users: one after another that would take over 3 seconds, so they really have to run at the same time.

Challenge 2

Fill in the function that starts a new task. The program should print letters: 14.

Challenge 3

Use a JoinSet to measure several files at once. Spawn one task per file into the set; each task returns (name, file_size(name).await). Wait for all of them with set.join_all().await (which returns them in finishing order), sort the results by name, and print:

a.txt: 5120 bytes
notes.md: 8192 bytes
photo_large.png: 15360 bytes
report.pdf: 10240 bytes

Previous: Running futures at the same time: join! and try_join! Next: Async channels: tokio::sync::mpsc