Async Rust: Tasks, Timeouts, Backpressure
Goal: you can spawn bounded concurrent work with tokio, impose per-job timeouts, and aggregate results — without unbounded spawn or blocking the reactor.
What async actually buys
Async = many tasks waiting on IO concurrently on a few threads. The executor
(tokio) polls futures that yield at .await points. What it does not do: make
CPU work parallel (that’s rayon/threads) or make slow dependencies fast.
The vocabulary you need daily
| Concept | In the lab |
|---|---|
async fn → Future | process_job — nothing runs until polled |
.await = yield point | tokio::time::sleep(...).await |
#[tokio::main] / #[tokio::test] | starts the runtime around main / the test |
| task = spawned future | set.spawn(async move { ... }) |
JoinSet | collect results as they finish (not in spawn order) |
Semaphore | the concurrency cap — acquire before spawn, permit released on drop |
tokio::time::timeout | per-job deadline; the whole point of “fail on time” |
Backpressure or it didn’t happen
The #1 async bug in services: for x in items { tokio::spawn(job(x)) } — 100k items
= 100k in-flight tasks, memory gone, upstream hammered. The fix is a cap before
spawn:
#![allow(unused)]
fn main() {
let permit = Arc::clone(&sem).acquire_owned().await.unwrap(); // waits when full
set.spawn(async move {
let _permit = permit; // held until the task completes
// ... work ...
});
}
JoinSet::join_next() drains outcomes; sort by id afterwards if order matters.
Timeouts are part of the design
#![allow(unused)]
fn main() {
match tokio::time::timeout(Duration::from_millis(500), job).await {
Ok(Ok(ms)) => JobOutcome::Done { id, ms },
Ok(Err(())) | Err(_) => JobOutcome::TimedOut { id },
}
}
Nested Results (timeout’s + the job’s) collapse into one explicit outcome enum —
errors as values again, now async-shaped.
The one blocking trap
Never call std::thread::sleep, heavy CPU loops, or std::fs on a runtime worker —
you stall every task sharing that thread. Use tokio::time::sleep, spawn_blocking,
or tokio::fs. In M6 the only sync moment is pool setup; queries are .awaited.
Lab
labs/m4-batch — 8 simulated jobs, concurrency 4, per-job timeout; prints each
outcome plus the wall-time vs serial-sum comparison. Tests cover: all-complete,
timeout-catch, 20 jobs through a limit of 3 (no losses), and the zero-concurrency
clamp.
Done when: cargo test -p m4-batch is green (4 tests) and you can explain what
happens to the permit when a task panics (dropped — the limit self-heals).