Tasks, channels, and shared state
Use tokio::spawn, select!, mpsc, and oneshot to coordinate async tasks. Examples cover shared state, timeouts, and cancellation.
Spawn tasks and wait for results#
Use tasks for independent work. Keep the JoinHandle when the result or completion matters.
tokio::spawn schedules a future and returns a JoinHandle<T>. Awaiting that handle returns Result<T, JoinError>; a panic or cancellation becomes a JoinError. Dropping a handle detaches the task: it keeps running until it finishes or the runtime shuts down.
use std::time::Duration;
#[tokio::main]
async fn main() -> Result<(), tokio::task::JoinError> {
let name = String::from("invoice");
let handle = tokio::spawn(async move {
tokio::time::sleep(Duration::from_millis(50)).await;
format!("{name} ready") // task owns name
});
let message = handle.await?; // handle may report a panic
println!("{message}");
Ok(())
}use tokio::task::JoinSet;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut tasks = JoinSet::new();
for job in 0..5 {
tasks.spawn(async move { (job, job * job) });
}
while let Some(result) = tasks.join_next().await {
let (job, answer) = result?;
println!("job {job}: {answer}");
}
Ok(())
}JoinSet aborts its remaining tasks when dropped. For ordinary handles, use explicit abort() then await the handle if you need cancellation to complete.join!, try_join!, and select!#
Wait for all operations with join!, propagate errors with try_join!, or respond to the first event with select!.
async fn load(id: u32) -> Result<u32, &'static str> {
if id == 0 { Err("bad id") } else { Ok(id * 10) }
}
#[tokio::main]
async fn main() -> Result<(), &'static str> {
let (a, b) = tokio::try_join!(load(1), load(2))?;
println!("{}", a + b);
Ok(())
}use std::time::Duration;
use tokio::sync::mpsc;
#[tokio::main]
async fn main() {
let (tx, mut rx) = mpsc::channel::<String>(8);
tx.send("ready".into()).await.unwrap();
tokio::select! {
Some(message) = rx.recv() => println!("{message}"),
_ = tokio::time::sleep(Duration::from_secs(2)) => {
println!("timed out");
}
}
}select! branch wins, other branch futures are dropped. Receiving with mpsc::Receiver::recv is cancellation safe; read_exact and write_all can lose partial progress if recreated in a loop. select! is concurrent within one task; it does not spawn tasks.mpsc: bounded work queues#
Multiple producers send jobs to one receiver. A bounded channel applies backpressure when the consumer falls behind.
The capacity limits queued items; send().await waits for space. Clone senders for more producers. recv().await returns None after all senders have dropped and queued messages have been consumed. One receiver can distribute work by spawning tasks after receiving.
use tokio::sync::mpsc;
#[tokio::main]
async fn main() {
let (tx, mut rx) = mpsc::channel::<u32>(16);
for producer in 0..2 {
let tx = tx.clone();
tokio::spawn(async move {
for job in 0..3 {
if tx.send(producer * 10 + job).await.is_err() {
break; // receiver was dropped
}
}
});
}
drop(tx); // release main's sender or recv never returns None
while let Some(job) = rx.recv().await {
println!("process {job}");
}
}mpsc::unbounded_channel() uses a synchronous send but has no queue limit. Avoid it if producers can outpace the consumer indefinitely.oneshot: request and reply#
Send exactly one result back to a caller, often combined with an mpsc request queue.
The sender uses synchronous send(value); the receiver uses .await. A dropped sender makes the receiver return an error. This is an easy way to give each queued request its own response.
use tokio::sync::{mpsc, oneshot};
struct Request {
value: u64,
reply: oneshot::Sender<u64>,
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let (tx, mut rx) = mpsc::channel::<Request>(8);
let worker = tokio::spawn(async move {
while let Some(Request { value, reply }) = rx.recv().await {
let _ = reply.send(value * value);
}
});
let (reply_tx, reply_rx) = oneshot::channel();
tx.send(Request { value: 12, reply: reply_tx }).await
.map_err(|_| std::io::Error::new(
std::io::ErrorKind::BrokenPipe, "worker closed"
))?;
println!("answer: {}", reply_rx.await?);
drop(tx);
worker.await?;
Ok(())
}Sleep, timeout, and intervals#
Tokio timers suspend tasks. A timeout stops waiting and drops its inner future when time expires.
use std::time::Duration;
use tokio::time::{sleep, timeout};
#[tokio::main]
async fn main() {
let result = timeout(Duration::from_millis(30), async {
sleep(Duration::from_secs(1)).await;
42
}).await;
assert!(result.is_err()); // Elapsed
}use std::time::Duration;
use tokio::time::{interval, MissedTickBehavior};
#[tokio::main]
async fn main() {
let mut ticks = interval(Duration::from_secs(5));
ticks.set_missed_tick_behavior(MissedTickBehavior::Skip);
for _ in 0..3 {
ticks.tick().await; // first tick is immediate
println!("refresh");
}
}timeout checks elapsed time before polling the inner future. Long synchronous work without yielding can exceed the deadline. interval defaults to burst behavior after missed ticks; choose Skip or Delay when bursts are undesirable.Cancellation and task lifetimes#
Cancellation is usually cooperative: a task notices a signal and returns after cleanup.
Dropping a future stops it at its next suspension point. JoinHandle::abort() requests cancellation and handle.await confirms completion. A CancellationToken (from tokio-util) lets many tasks choose their own cleanup path.
[dependencies]
tokio = { version = "1", features = ["full"] }
tokio-util = { version = "0.7", features = ["rt"] }use std::time::Duration;
use tokio_util::sync::CancellationToken;
#[tokio::main]
async fn main() -> Result<(), tokio::task::JoinError> {
let token = CancellationToken::new();
let worker_token = token.clone();
let worker = tokio::spawn(async move {
loop {
tokio::select! {
_ = worker_token.cancelled() => break,
_ = tokio::time::sleep(Duration::from_secs(1)) => {
println!("heartbeat");
}
}
}
println!("flush buffered work here");
});
token.cancel();
worker.await?;
Ok(())
}abort() drops a task when it next yields; Rust destructors run, but async cleanup code cannot run after an abrupt abort. Blocking work in spawn_blocking generally cannot be stopped by abort once running.Help improve CodeRef
Allow Google Analytics to measure page visits and use of the guides? It uses cookies after you accept. Your choice won’t affect access to the site. Privacy notice