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 await markers 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.