Why choose async/await over threads?
Conceptual models: threads, async/await, fibers
- Many distinguish:
- Threads = OS‑scheduled, preemptive, parallelism.
- Async/await = stackless coroutines and futures; cooperative concurrency.
- Fibers/green threads = stackful user‑mode threads; look like blocking code but cooperatively scheduled.
- Some argue a better comparison is async/await vs fibers, since both multiplex I/O on few OS threads.
- Others note that async/await is “just” CPS/state machines with sugar; same underlying I/O primitives.
Cancellation and cleanup
- A major theme: cancellation semantics.
- Async Rust commonly “cancels” by dropping futures, which may interrupt operations without giving them a chance to react, causing subtle bugs and resource leaks.
- Fiber models often use explicit cancellation signals and cooperative handling at yield/IO points, giving tasks a chance to clean up, finish critical sections, or ignore cancellation.
- Async Drop in Rust is seen as desirable but technically hard; some see it as strictly weaker than explicit cancellation signals.
Composability and “function coloring”
- Async is called “viral”: once a function is async, callers must also be async, splitting APIs into sync vs async flavors and further into runtime flavors (Tokio, async‑std, etc.).
- This is contrasted with fibers/threads, where concurrency can be an internal detail and public APIs remain synchronous.
- Some see
awaitmarkers as helpful explicit suspension points; others view them as noise that doesn’t correspond well to “where I/O happens” or to real interference points.
Performance, memory, and scalability
- Pro‑async points:
- Much lower per‑task memory than OS threads (no big stacks), enabling millions of concurrent tasks.
- Less kernel context‑switch overhead; good for high‑concurrency servers and embedded targets without threads or OS.
- Compiler‑generated state machines can avoid dynamic allocation and deliver very low latency in some workloads.
- Skeptical views:
- For many real‑world servers, cache misses and I/O dominate; async micro‑optimizations may not matter.
- Thread‑per‑client is bad, but user‑mode green threads or processes plus evented I/O can give similar wins with simpler semantics.
- Async can worsen reasoning about backpressure and cancellation, and debugging “stuck” tasks is harder.
FFI, runtimes, and ecosystem fragmentation
- Fibers and M:N threading complicate FFI: TLS, stack growth, and assumptions about OS threads often break C libraries.
- Async Rust’s executor‑driven futures avoid some of this but create fragmentation:
- Sync vs async crates; and among async crates, dependence on specific executors.
- Hard to write libraries that are executor‑agnostic and composable with both sync and async worlds.
Single‑threaded vs multi‑threaded async
- Single‑thread event loops (Node, many embedded runtimes) avoid data races but still have logical races; synchronization is still needed for shared state.
- Rust’s default async ecosystem (e.g., Tokio) is multi‑threaded; many see that as combining async complexity with full multithreading complexity.
- Some wish the default had been single‑threaded, with explicit opt‑in to multithreaded executors.
Use cases and ergonomics
- Async/await is praised for:
- High‑concurrency I/O services (web servers, proxies).
- Embedded and bare‑metal systems without OS threads.
- GUI and “single‑threaded but responsive” contexts.
- Others prefer:
- Threads or green threads for CPU‑bound work and actor‑style architectures.
- Processes plus message passing for robustness and simpler mental models.
- Several comments complain that in Rust, async/await dominates I/O libraries, effectively forcing it even when threads or processes would suffice.
Overall sentiment
- Strong split:
- Enthusiasts see async/await as a powerful, expressive concurrency tool that scales better than threads and works in no‑OS environments.
- Critics see it as ergonomically awkward, ecosystem‑splitting, hard to cancel and debug, and often an over‑optimization where simpler thread‑ or process‑based designs would do.