Why choose async/await over threads?

Async/await as a concurrency model is being weighed against OS threads, green threads and fibers, with Rust as a focal example. Proponents highlight async’s ability to scale to huge numbers of tasks with low memory overhead, explicit suspension points, and suitability for environments without threads (e.g. embedded), while critics point to “function colouring,” ecosystem fragmentation by runtimes, tricky cancellation semantics, and FFI complications. Many argue the real trade‑off is between different abstractions over the same underlying I/O primitives, and that each model—threads, async/await, or fibers—is best suited to specific workloads rather than being a universal replacement for the others.

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.