Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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

ConceptIn the lab
async fnFutureprocess_job — nothing runs until polled
.await = yield pointtokio::time::sleep(...).await
#[tokio::main] / #[tokio::test]starts the runtime around main / the test
task = spawned futureset.spawn(async move { ... })
JoinSetcollect results as they finish (not in spawn order)
Semaphorethe concurrency cap — acquire before spawn, permit released on drop
tokio::time::timeoutper-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).