--- name: rust-async-tokio-idioms description: Tokio 1.x runtime idioms — spawn vs block_in_place, cancellation, channels, avoiding Arc> patterns. when_to_use: You're writing async Rust code with Tokio. Applies to nearly all clawmates crates. tags: [rust, async, tokio] --- # Tokio async idioms Anchored to **Tokio 1.x** (workspace tracks the latest 1.x line, currently ~1.42). ## Runtime choice - **`#[tokio::main]`** in the binary. Default is multi-thread — fine. - **`#[tokio::test]`** in tests. Default is current-thread (fast, deterministic). Add `flavor = "multi_thread"` when the code under test spawns. - **NEVER** call `tokio::runtime::Handle::current().block_on(...)` inside async code — deadlocks in current-thread, breaks structured concurrency in multi-thread. ## Spawning ```rust let handle = tokio::spawn(async move { do_work().await }); let result = handle.await??; // ?? = JoinError, then inner Result ``` - **`tokio::spawn`** for detached background work. Returns `JoinHandle`. - **`tokio::task::spawn_blocking`** for CPU-heavy or blocking-syscall work (`std::fs`, `sync::Mutex`). Bounded pool — don't spawn thousands. - **`tokio::task::spawn_local`** only inside a `LocalSet` — you almost never want this. ## Cancellation - Dropping a `JoinHandle` DOES NOT cancel the task by default. Use `.abort()` or `CancellationToken`. - Prefer `tokio_util::sync::CancellationToken` for structured cancel — pass the child token, call `parent.cancel()` at teardown. - `tokio::select!` on `token.cancelled()` in every long-running loop. ## Channels - **`mpsc::channel(cap)`** for actor-like state — one owner processes messages. Cap ≥ number of expected concurrent senders. - **`oneshot::channel()`** for request/response. Sender is consumed on send. - **`broadcast::channel(cap)`** for fan-out. Slow subscribers get `Lagged` errors — handle it. - **`watch::channel(initial)`** for latest-value-wins state (config, health flags). ## Avoiding `Arc>` Common trap: shared mutable state protected by a lock. Usually a smell. - Immutable snapshot: `Arc` is enough. - Update-then-read cadence: `arc-swap::ArcSwap` (lock-free). - Actor-shaped: `mpsc::channel` + one task owning the state. - Sync mutex needed AT ALL: use `std::sync::Mutex` (not `tokio::sync::Mutex`) for anything held < 1ms. Tokio's mutex is for holding across `.await`, which you should be actively avoiding. ## `.await` discipline - Never `.await` inside a `std::sync::MutexGuard` scope — deadlock waiting to happen. Drop the guard first. - Never `.await` inside a `tokio::task::block_in_place` block — it panics. - `tokio::select! { biased; ... }` when you need branch order guarantees (usually shutdown-first). ## Anti-patterns - **`futures::executor::block_on` inside a Tokio task.** Use `tokio::task::block_in_place` if you really need to sync-bridge. - **`std::thread::sleep` in async code.** Use `tokio::time::sleep`. - **Spawning from `Drop`.** `Drop` isn't async; the runtime may already be shutting down.