Python Async, ¿por qué todavía no es algo mainstream?
(tonybaloney.github.io)Python Async, ¿por qué todavía no es algo mainstream?
asyncio de Python es una herramienta poderosa que puede reducir los tiempos de espera y aumentar de forma drástica la eficiencia de un programa en entornos con muchas tareas de I/O (entrada/salida). Sin embargo, a pesar de sus varias ventajas, no todos los desarrolladores de Python lo usan activamente, y hay algunas razones de fondo para ello.
1. Se vuelve más difícil de manejar mentalmente: carga cognitiva
La barrera más grande es la complejidad. El código síncrono es intuitivo porque podemos seguir el flujo de ejecución de arriba hacia abajo, en orden, como si estuviéramos leyendo un libro.
Pero el código asíncrono es diferente. Es como un cocinero que, mientras hierve la pasta para preparar un plato (tiempo de espera), al mismo tiempo hace la salsa y, en los ratos libres, corta las verduras. Al final puede terminar la comida más rápido, pero en la cabeza del cocinero hay que estar pendiente constantemente de varias tareas: “¿qué tan cocida va la pasta?”, “¿no se estará quemando la salsa?”, etc.
Como el flujo de ejecución del código va saltando de un lado a otro, se vuelve difícil entender qué tarea se está ejecutando en ese momento y cuál está en espera. Esto hace que el proceso de depuración, especialmente cuando aparece un bug, sea mucho más complicado para rastrear la causa.
2. Un ecosistema dividido: compatibilidad de librerías
Otro problema de Python es que su ecosistema de librerías está dividido entre “síncrono” y “asíncrono”. Muchas librerías famosas y convenientes (por ejemplo, la librería estándar de peticiones HTTP requests o muchos ORM) solo funcionan de manera síncrona.
Si usas mal una librería síncrona dentro de código asíncrono, durante la ejecución de ese código síncrono se detiene por completo el “event loop”, que es justamente la mayor ventaja del modelo asíncrono. Es como construir varios carriles y terminar usando solo uno, así que deja de tener sentido usar async. Para resolver este problema hay que aprender y adoptar librerías aparte con soporte asíncrono (aiohttp, asyncpg, etc.), y eso vuelve más empinada la curva de aprendizaje.
3. Solo golpea a uno a la vez: GIL (bloqueo global del intérprete)
El GIL (Global Interpreter Lock) es una de las características problemáticas más conocidas de Python: incluso si hay varios hilos dentro de un mismo proceso, limita la ejecución a un solo hilo a la vez. Como asyncio funciona en un solo hilo, no choca directamente con el GIL, pero la existencia del GIL sí limita el alcance de uso de async.
asyncio está optimizado para aprovechar los tiempos de espera de I/O (esperar respuestas de red, esperar lectura de archivos, etc.). Pero si dentro de una función async se incluye una tarea intensiva de CPU con cálculos muy complejos, todo el event loop se detiene hasta que ese cálculo termina. Durante ese tiempo, las demás tareas de I/O no pueden hacer nada y solo les queda esperar. En última instancia, para trabajos que sí requieren paralelismo real, sigue existiendo la limitación de tener que usar otras tecnologías como multiprocessing.
4. Una esperanza para el futuro: Python 3.14 y la eliminación del GIL
Pero hay noticias muy prometedoras sobre estas limitaciones. Se trata del movimiento hacia la eliminación opcional del GIL, introducido de forma experimental desde Python 3.13 y que se espera que madure más en la versión 3.14.
Este cambio, impulsado mediante la propuesta PEP 703, busca permitir que los desarrolladores puedan ejecutar código Python sin GIL si así lo desean. Si esto se vuelve realidad, Python podrá habilitar multithreading real, donde varios hilos aprovechen varios núcleos de CPU al mismo tiempo.
Esto podría generar una sinergia enorme cuando se use junto con asyncio. Las tareas de I/O podrían manejarse eficientemente con asyncio, mientras que las tareas de CPU más pesadas podrían enviarse a hilos separados para procesarse en paralelo sin las restricciones del GIL. Se espera que este cambio marque un gran punto de inflexión para el ecosistema de Python y derribe muchas de las barreras que han frenado la adopción de la programación async.
9 comentarios
Siento que lo del GIL sale un poco de la nada... Incluso si se elimina el GIL,
si quieres usar multithreading tanto en casos I/O bound como CPU bound,
quizás sería mejor adoptar otra alternativa que no sea Python...
También me da la impresión de que a quienes se meten a fondo con Python no les gusta mucho
asyncio.Creo haber escuchado seguido la opinión de que
geventdebería haberse vuelto la opción principal.Estoy de acuerdo en que no podemos esperar que la dirección actual respecto al GIL llegue a ser tan buena como para no quedar en desventaja frente a "otras alternativas".
Pero decir que hay que adoptar otra alternativa en lugar de Python no debería llevar a un tono de que no hay problema, sino más bien a un tono de que sí hay un problema.
Se usa bastante
asyncio... sirve bastante bien... tiene la limitación de que la cancelación de tareas es edge-triggered (no level-triggered), pero en realidad no es tan común escribir código que sea consciente de la cancelación de tareas y lo maneje de forma elegante; más bien, el problema más grande es que el event loop mantiene referencias débiles a las tareas, así que pueden desaparecer por el GC... pero eso se resuelve con structured concurrency.Para casi cualquier trabajo importante relacionado con I/O no hay problema para encontrar librerías con soporte para
asyncio...¿El GIL? No tiene mucho que ver... de por sí, la idea de usar
asynciopara paralelizar trabajo intensivo de CPU es algo rara... si el GIL mejora, entonces sí será útil para multithreading intensivo de CPU... async se trata de aprovechar al máximo los tramos con cuello de botella de I/O...En fin, la conclusión... aunque hay algunos problemas de diseño, lo estamos usando bien en producción sin mayores inconvenientes para cumplir el objetivo.
¿Alguna vez les ha pasado que el GC recolecte sus tareas?
Claro, yo también uso
asynciohasta el cansancio en producción, pero la experiencia de uso actual no me deja lo suficientemente satisfecho como para decir que "lo estoy aprovechando bien".El
asyncioactual está diseñado asumiendo el GIL y, visto de algún modo, es una estrategia para esquivarlo, así que el GIL no interactúa directamente conasyncio.Pero, si lo vemos desde la perspectiva de toda la programación concurrente que funciona sobre
asyncio, creo que decir que el GIL no tiene relación termina sonando como “como es Python, es obvio que no se puede”.Yo solo usaré joblib.
El problema de
asynciono es la dificultad de la programación asíncrona, sino su baja calidad. Un diseño que tira por la borda la consistencia y la universalidad no es algo raro en Python, pero cosas comoProactorEventLooptodavía tienen bugs que provocan caídas del servicio y que fueron reportados hace 5 años.Para quienes estamos obligados a usarlo, es bastante difícil tomarse a la ligera un texto como este.
Por supuesto, una razón más importante podría ser que, debido al GIL, el beneficio que se puede obtener desde un principio es menor en comparación con otros entornos.
Creo que decir que, sin el GIL, se podría generar sinergia es casi engañoso. Si a un corredor al que le falta una pierna le pones una prótesis para que, aunque sea con incomodidad, pueda correr, ¿eso es "sinergia"?