¿Por qué elegir async/await en lugar de hilos?

Modelos conceptuales: hilos, async/await, fibras

  • Muchos distinguen:
    • Hilos = programados por el SO, preventivos, paralelismo.
    • Async/await = corrutinas sin pila y futures; concurrencia cooperativa.
    • Fibras/hilos verdes = hilos en modo usuario con pila; parecen código bloqueante pero se programan cooperativamente.
  • Algunos sostienen que una mejor comparación es async/await frente a fibras, ya que ambos multiplexan E/S sobre pocos hilos del SO.
  • Otros señalan que async/await es “solo” CPS/máquinas de estados con azúcar sintáctico; mismas primitivas de E/S subyacentes.

Cancelación y limpieza

  • Un tema importante: la semántica de cancelación.
    • En Rust asíncrono, a menudo se “cancela” soltando futures, lo que puede interrumpir operaciones sin darles oportunidad de reaccionar, causando errores sutiles y fugas de recursos.
    • Los modelos con fibras suelen usar señales explícitas de cancelación y manejo cooperativo en puntos de yield/E/S, dando a las tareas la oportunidad de limpiar, terminar secciones críticas o ignorar la cancelación.
  • Async Drop en Rust se ve como deseable pero técnicamente difícil; algunos lo consideran estrictamente más débil que las señales explícitas de cancelación.

Componibilidad y “coloración de funciones”

  • Se dice que async es “viral”: una vez que una función es async, los llamadores también deben ser async, dividiendo las API en variantes síncronas y asíncronas, y además en variantes de runtime (Tokio, async‑std, etc.).
  • Esto se contrasta con fibras/hilos, donde la concurrencia puede ser un detalle interno y las API públicas permanecen síncronas.
  • Algunos ven los marcadores await como puntos de suspensión explícitos útiles; otros los consideran ruido que no corresponde bien a “dónde ocurre la E/S” ni a puntos reales de interferencia.

Rendimiento, memoria y escalabilidad

  • Puntos a favor de async:
    • Mucho menor uso de memoria por tarea que los hilos del SO (sin pilas grandes), permitiendo millones de tareas concurrentes.
    • Menor sobrecarga de cambios de contexto del kernel; bueno para servidores de alta concurrencia y objetivos embebidos sin hilos o sin SO.
    • Las máquinas de estados generadas por el compilador pueden evitar asignación dinámica y ofrecer latencia muy baja en algunas cargas de trabajo.
  • Visiones escépticas:
    • Para muchos servidores reales, los fallos de caché y la E/S dominan; las microoptimizaciones de async pueden no importar.
    • Un hilo por cliente es malo, pero los hilos verdes en modo usuario o procesos más E/S basada en eventos pueden dar ventajas similares con semánticas más simples.
    • Async puede empeorar el razonamiento sobre backpressure y cancelación, y depurar tareas “atascadas” es más difícil.

FFI, runtimes y fragmentación del ecosistema

  • Las fibras y el threading M:N complican FFI: TLS, crecimiento de pila y supuestos sobre hilos del SO a menudo rompen bibliotecas C.
  • Los futures dirigidos por ejecutor de Rust asíncrono evitan parte de esto pero crean fragmentación:
    • Crates síncronos vs asíncronos; y, entre crates asíncronos, dependencia de ejecutores específicos.
    • Es difícil escribir bibliotecas agnósticas al ejecutor y componibles tanto con el mundo síncrono como con el asíncrono.

Async de un solo hilo vs multihilo

  • Los bucles de eventos de un solo hilo (Node, muchos runtimes embebidos) evitan carreras de datos, pero siguen teniendo carreras lógicas; la sincronización sigue siendo necesaria para el estado compartido.
  • El ecosistema async por defecto de Rust (por ejemplo, Tokio) es multihilo; muchos ven eso como combinar la complejidad de async con la complejidad completa del multithreading.
  • Algunos desearían que el valor por defecto hubiera sido de un solo hilo, con una opción explícita para usar ejecutores multihilo.

Casos de uso y ergonomía

  • Async/await es elogiado por:
    • Servicios de E/S de alta concurrencia (servidores web, proxies).
    • Sistemas embebidos y bare metal sin hilos del SO.
    • Contextos de interfaz gráfica y “de un solo hilo pero responsivos”.
  • Otros prefieren:
    • Hilos o hilos verdes para trabajo ligado a CPU y arquitecturas estilo actor.
    • Procesos más paso de mensajes para robustez y modelos mentales más simples.
  • Varios comentarios se quejan de que en Rust async/await domina las bibliotecas de E/S, forzándolo efectivamente incluso cuando hilos o procesos serían suficientes.

Sentimiento general

  • Hay una división fuerte:
    • Los entusiastas ven async/await como una herramienta de concurrencia potente y expresiva que escala mejor que los hilos y funciona en entornos sin SO.
    • Los críticos lo ven como incómodo ergonómicamente, fragmentador del ecosistema, difícil de cancelar y depurar, y a menudo una sobreoptimización donde bastarían diseños más simples basados en hilos o procesos.