¿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
awaitcomo 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.