Tokio is the de facto async runtime behind axum, reqwest, tonic and most Rust network services.
Tokio provides the executor, timers, async networking and filesystem APIs, and sync primitives. Add it with cargo add tokio --features full (or pick features like rt-multi-thread, macros, net, time, sync).
#[tokio::main] turns async fn main into a normal main that starts a multi-threaded, work-stealing runtime and blocks on your future. #[tokio::test] does the same for tests.
Concurrency tools:
- `tokio::spawn(fut)` runs a future as an independent task, possibly on another worker thread, and returns a
JoinHandleyou can.awaitfor the result (anErrif it panicked). Spawned futures must beSend + 'static, somoveowned orArc'd data in. - `tokio::join!(a, b)` runs futures concurrently within the current task and waits for all;
try_join!stops at the first error. - `tokio::select!` waits on several futures and runs the branch of whichever finishes first, dropping (cancelling) the others. It is the building block for timeouts, shutdown signals and racing requests.
- `tokio::time::timeout(dur, fut)` and
sleepfor timing; neverstd::thread::sleep.
For shared state, std::sync::Mutex is fine if you never hold the guard across an .await (the compiler usually stops you anyway, because the guard is not Send). If you must hold a lock across .await, use tokio::sync::Mutex. Async channels live in tokio::sync: mpsc, oneshot for a single reply, broadcast, and watch for latest-value state.
Blocking or CPU-heavy work goes to `tokio::task::spawn_blocking`, which runs it on a separate thread pool and gives you a future for the result.
A useful mental model: join! and select! give concurrency inside one task; spawn gives you new tasks that can run in parallel.
Example
// Cargo.toml: tokio = { version = "1", features = ["full"] }
use std::time::Duration;
use tokio::{sync::mpsc, time::{sleep, timeout}};
async fn fetch(id: u32, ms: u64) -> String {
sleep(Duration::from_millis(ms)).await; // yields; never use thread::sleep here
format!("item {id}")
}
#[tokio::main]
async fn main() {
let (a, b) = tokio::join!(fetch(1, 30), fetch(2, 10)); // concurrent, ~30 ms total
println!("{a}, {b}"); // item 1, item 2
let slow = timeout(Duration::from_millis(5), fetch(3, 50)).await;
println!("timed out: {}", slow.is_err()); // timed out: true
let (tx, mut rx) = mpsc::channel(16);
for id in 0..3 {
let tx = tx.clone();
tokio::spawn(async move { tx.send(fetch(id, 5).await).await.unwrap() });
}
drop(tx);
let mut got = Vec::new();
while let Some(item) = rx.recv().await { got.push(item); }
let sum = tokio::task::spawn_blocking(|| (1..=1_000_000u64).sum::<u64>()).await.unwrap();
println!("{} items, sum = {sum}", got.len()); // 3 items, sum = 500000500000
}Remember
#[tokio::main]starts the runtime;tokio::spawncreatesSend + 'statictasksjoin!waits for all,select!takes the first and cancels the rest- Use
tokio::timeandspawn_blocking; never block a worker thread
Watch out for
- Holding a
std::sync::MutexGuardacross.await; scope the guard or usetokio::sync::Mutex.