async / await
Rusts async story: async fn returns a Future that does no work until polled by an executor. Tokio is the standard executor for servers and CLIs; smol/embassy fit smaller targets. Async unlocks high-concurrency I/O with very little memory per task, but the borrow checker, Send bounds, and lifetimes are stricter inside async than in sync code.
Concurrent fetches, timeouts, channels, with Tokio
EXAMPLE
// Cargo.toml
// [dependencies]
// tokio = { version = "1", features = ["full"] }
// reqwest = "0.12"
// futures = "0.3"
use std::time::Duration;
use tokio::{time, sync::mpsc};
use futures::future::join_all;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// 1) Run several fetches concurrently
let urls = vec![
"https://example.com",
"https://example.org",
"https://example.net",
];
let client = reqwest::Client::new();
let bodies = join_all(urls.into_iter().map(|u| {
let client = client.clone();
async move {
let res = time::timeout(Duration::from_secs(5), client.get(u).send()).await??;
let bytes = res.bytes().await?;
Ok::<_, Box<dyn std::error::Error + Send + Sync>>(bytes.len())
}
})).await;
for (i, r) in bodies.into_iter().enumerate() {
println!("{i}: {:?}", r.map(|n| format!("{n} bytes")));
}
// 2) Producer/consumer via mpsc — back-pressure built in
let (tx, mut rx) = mpsc::channel::<u32>(16);
let producer = tokio::spawn(async move {
for i in 0..10 {
tx.send(i).await.unwrap();
time::sleep(Duration::from_millis(50)).await;
}
});
let consumer = tokio::spawn(async move {
while let Some(n) = rx.recv().await {
println!("got {n}");
}
});
let _ = tokio::try_join!(producer, consumer)?;
// 3) Select on multiple futures — first one to finish wins
let result = tokio::select! {
v = slow_op() => format!("slow: {v}"),
_ = time::sleep(Duration::from_millis(100)) => "timed out".to_string(),
};
println!("{result}");
// 4) CPU-bound work inside async: hand it off to a blocking thread
let answer = tokio::task::spawn_blocking(|| {
let mut sum: u64 = 0;
for i in 0..1_000_000_u64 { sum = sum.wrapping_add(i.wrapping_mul(31)); }
sum
}).await?;
println!("answer={answer}");
Ok(())
}
async fn slow_op() -> u32 {
time::sleep(Duration::from_millis(250)).await;
42
}
Why it matters
CPU-bound work inside async (image resize, JSON parse of huge blob, hashing) stalls the executor and tanks tail latency. Wrap it in spawn_blocking so it runs on the blocking pool, then await the JoinHandle — async stays responsive while the heavy work proceeds.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
async fn fetch_id() -> u32 { 42 }
#[tokio::main]
async fn main() {
let id = fetch_id().await;
println!("id = {id}");
}
Try it Yourself »
Discussion
Loading…