Tokio
Tokio is Rusts mainstream async runtime: a multi-threaded scheduler, an async I/O reactor, timers, channels, and a synchronisation toolkit. Pick it for any non-trivial async work — Axum, Reqwest, SQLx, and Tonic all build on it. Configure the runtime macro (#[tokio::main]) to choose multi-threaded vs current-thread.
Tokio runtime, tasks, channels, timeouts, JoinSet
EXAMPLE
// Cargo.toml
// [dependencies]
// tokio = { version = "1", features = ["full"] }
use std::time::Duration;
use tokio::{
sync::{mpsc, Semaphore, oneshot},
task::JoinSet,
time,
};
#[tokio::main(flavor = "multi_thread", worker_threads = 4)]
async fn main() -> anyhow::Result<()> {
// 1) Spawn — fire and forget, returns a JoinHandle you can await later
let handle = tokio::spawn(async {
time::sleep(Duration::from_millis(50)).await;
42
});
println!("task returned {}", handle.await?);
// 2) Channels — mpsc (multi-producer single-consumer) with backpressure
let (tx, mut rx) = mpsc::channel::<u32>(32);
let producer = tokio::spawn(async move {
for i in 0..10 {
tx.send(i).await.unwrap();
}
});
let consumer = tokio::spawn(async move {
while let Some(n) = rx.recv().await {
println!("got {n}");
}
});
let _ = tokio::try_join!(producer, consumer);
// 3) Oneshot — single-use reply channel, common for request/response
let (tx, rx) = oneshot::channel::<&str>();
tokio::spawn(async move {
time::sleep(Duration::from_millis(50)).await;
let _ = tx.send("done");
});
println!("oneshot: {}", rx.await?);
// 4) Timeout — wrap any future
match time::timeout(Duration::from_millis(20), slow()).await {
Ok(v) => println!("got: {v}"),
Err(_) => println!("timed out"),
}
// 5) Concurrency limit with Semaphore — pool around a finite resource
let sem = std::sync::Arc::new(Semaphore::new(3));
let mut joins = JoinSet::new();
for i in 0..10 {
let sem = sem.clone();
joins.spawn(async move {
let _permit = sem.acquire().await.unwrap();
time::sleep(Duration::from_millis(30)).await;
i * 2
});
}
let mut results = Vec::new();
while let Some(r) = joins.join_next().await {
results.push(r?);
}
results.sort();
println!("results: {results:?}");
// 6) CPU-bound work goes on the blocking pool, not the async pool
let n = tokio::task::spawn_blocking(|| {
(0..1_000_000_u64).fold(0u64, |a, x| a.wrapping_add(x.wrapping_mul(31)))
})
.await?;
println!("sum = {n}");
// 7) Graceful shutdown — cancel + drain remaining tasks
let (cancel_tx, mut cancel_rx) = mpsc::channel::<()>(1);
let worker = tokio::spawn(async move {
loop {
tokio::select! {
_ = time::sleep(Duration::from_millis(20)) => println!("tick"),
_ = cancel_rx.recv() => { println!("shutdown"); break; }
}
}
});
time::sleep(Duration::from_millis(80)).await;
let _ = cancel_tx.send(()).await;
let _ = worker.await;
Ok(())
}
async fn slow() -> &"static str {
time::sleep(Duration::from_millis(100)).await;
"ok"
}
Why it matters
JoinSet is the right abstraction when you spawn N tasks and need to consume their results as they finish. It avoids the boilerplate of carrying Vec
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
use tokio::time::{sleep, Duration};
#[tokio::main]
async fn main() {
sleep(Duration::from_millis(500)).await;
println!("woke up");
}
Try it Yourself »
Discussion
Loading…