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:
'static: the task may outlive the function that spawned it, so it can't borrow local variables. Pass owned values (ascheck_servertakes aString), or write anasync move { ... }block, which moves the variables it uses into the future, just like amoveclosure.Send: a task can pause at an.awaiton one worker thread and resume on another. So everything the task holds across an.awaitmust beSend. AnRckept alive over an.awaitbreaks that:
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
- Borrowing locals in a spawned task:
E0373, "async block may outlive the current function", with a help line suggestingmove. Move owned data in (async move). - Holding an
Rc, aRefCellborrow or astdMutexGuardacross.awaitin a spawned task: "future cannot be sent between threads safely". - Printing from inside tasks: the order changes between runs. Return values and print after awaiting.
- Not awaiting the handles: work still running when
mainends is lost.
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