El Modelo Mental del Bucle de Eventos
La concurrencia de Asyncio confunde a las personas que ya conocen los hilos, porque desde fuera parece paralelismo pero funciona bajo un principio completamente diferente.
Busca en todas las páginas de la documentación
La concurrencia de Asyncio confunde a las personas que ya conocen los hilos, porque desde fuera parece paralelismo pero funciona bajo un principio completamente diferente.
Todas las demás páginas de esta sección (coroutines, tasks, gather, structured concurrency, sync/async bridging) son en realidad un solo mecanismo visto desde un ángulo diferente: un único programador ejecutando una pieza de código Python a la vez, cambiando a otra pieza exactamente cuando la actual dice que no tiene nada útil que hacer en este momento.
Entender ese programador, no memorizar la superficie de la API, es lo que hace que asyncio.gather frente a asyncio.TaskGroup frente a asyncio.to_thread parezcan una idea coherente en lugar de tres herramientas no relacionadas que hay que consultar.
Una corrutina es lo que obtienes al llamar a una función async def, no el resultado de ejecutarla, sino un objeto de función pausado en su primera línea, esperando a ser impulsado hacia adelante.
Nada dentro de una corrutina se ejecuta realmente hasta que algo la espera o la programa como una Task.
El bucle de eventos es la pieza que hace el impulso: mantiene una cola de corrutinas y callbacks listas para ejecutarse, ejecuta una hasta que encuentra un await en algo que aún no ha terminado, y luego pasa al siguiente elemento listo en la cola.
await es el punto exacto de traspaso: significa "No tengo nada útil que hacer hasta que esto termine, así que deja que otra cosa se ejecute mientras tanto", que es todo el mecanismo sobre el que descansa la programación cooperativa.
Una analogía útil es un solo chef dirigiendo varias ollas en una sola estufa: el chef revuelve la olla A, luego mientras la olla A hierve sin supervisión, el chef pasa a la olla B, luego a la olla C, siempre haciendo exactamente una cosa en cualquier instante, pero nunca quedándose inactivo esperando a que una sola olla termine.
Eso es fundamentalmente diferente de contratar tres chefs (hilos) que pueden revolver genuinamente tres ollas al mismo tiempo, o alquilar tres cocinas (procesos) que ni siquiera comparten una estufa.
Asyncio es el modelo de un solo chef: eficiente porque el chef nunca está inactivo mientras otra cosa podría progresar, pero aún limitado por el hecho de que hay exactamente un chef haciendo exactamente una cosa a la vez.
La cola de listos del bucle es el mecanismo que convierte "muchas esperas pendientes" en "alto rendimiento en un solo hilo": una corrutina esperando una lectura de socket no ocupa el bucle mientras la red está en silencio, registra interés y se aparta, y el bucle gasta ese tiempo ejecutando otras corrutinas cuyos datos ya han llegado.
Una Task es lo que envuelve una corrutina para que el bucle pueda rastrearla y reanudarla de forma independiente: asyncio.create_task programa una corrutina para que se ejecute en el bucle sin que el llamador la espere inmediatamente, que es lo que hace posible la ejecución concurrente de múltiples corrutinas en primer lugar.
Aquí es también donde las dos formas de ejecutar varias corrutinas juntas divergen realmente, y es una fuente común de sorpresa: asyncio.gather no cancela los otros awaitables cuando uno de ellos lanza una excepción; por defecto, propaga inmediatamente esa primera excepción mientras que los demás siguen ejecutándose en segundo plano, sin supervisión, a menos que el llamador haga algo al respecto.
asyncio.TaskGroup (3.11+) existe específicamente para cerrar esa brecha: vincula la vida de cada tarea hija al bloque async with que la contiene, y si un hijo lanza una excepción, el grupo cancela activamente a sus hermanos antes de que el bloque salga, una diferencia de comportamiento real, no solo una sintaxis más agradable sobre la misma garantía.
"Bloquear el bucle" es el fallo de modelo mental más importante en el código asyncio: cualquier llamada síncrona que no ceda (un bucle de CPU intensivo, una lectura de archivo bloqueante, un controlador de base de datos síncrono) ocupa al chef por completo, por lo que todas las demás corrutinas programadas en ese bucle se detienen durante exactamente el tiempo que dura esa llamada, no solo la corrutina que la hizo.
import asyncio
async def blocks_everyone() -> None:
total = 0
for i in range(50_000_000): # CPU pura, nunca cede
total += i
# todas las demás corrutinas en este bucle esperaron todo el tiempo
async def cooperates() -> None:
await asyncio.to_thread(lambda: sum(range(50_000_000)))
# el trabajo de CPU se ejecutó en un hilo de trabajo; el bucle siguió sirviendo a otrosEl puente síncrono/asíncrono (asyncio.to_thread, loop.run_in_executor, asyncio.run) es el mecanismo para cruzar entre "código que el bucle está dirigiendo" y "código que no puede cooperar", en ambas direcciones: descargar una llamada bloqueante a un pool de hilos para que el bucle se mantenga libre, o entrar al bucle desde código síncrono ordinario a través de asyncio.run.
Una sutileza que confunde a ingenieros experimentados que pasan de hilos a asyncio: hay exactamente un bucle de eventos por hilo. asyncio.run crea uno, lo ejecuta hasta su finalización y lo cierra, y no puedes anidar una segunda llamada a asyncio.run dentro de un bucle en ejecución; los servidores en su lugar mantienen un bucle de larga duración para la vida del proceso en lugar de crear uno por solicitud.
La equidad de la programación del bucle también depende completamente de que cada corrutina ceda en intervalos razonables; una base de código async que se comporta bien es realmente una base de código donde cada operación adyacente a E/S es nativamente async o explícitamente puenteada a un hilo, porque el bucle no puede prevenir una corrutina que se niega a esperar.
Existen implementaciones alternativas de bucles de eventos para el mismo contrato de programación con diferentes características de rendimiento: uvloop, por ejemplo, reemplaza el bucle predeterminado de Python puro con uno respaldado por libuv para E/S de menor latencia, sin cambiar nada sobre el modelo de corrutina/Task/await descrito aquí.
El Global Interpreter Lock (GIL) todavía importa dentro de un solo bucle asyncio: incluso en la compilación experimental de CPython con hilos libres introducida en 3.13 y continuada en 3.14, el contrato de asyncio de "el bytecode de una corrutina se ejecuta a la vez en este bucle" no cambia; los hilos libres afectan al paralelismo multicores real de hilos del sistema operativo separados, no al modelo de programación cooperativa de un solo bucle en sí.
La observabilidad importa más en asyncio que en el código síncrono precisamente porque los fallos son fallos de programación, no solo fallos de lógica: el modo de depuración de asyncio (PYTHONASYNCIODEBUG=1 o asyncio.run(main(), debug=True)) expone callbacks lentos y corrutinas nunca esperadas que de otro modo fallarían silenciosamente.
| Enfoque | Fortaleza | Debilidad | Mejor Ajuste |
|---|---|---|---|
Corrutinas nativas async/await | Baratas de programar, miles de esperas concurrentes en un hilo | Inútiles para trabajo limitado por CPU; cada dependencia debe cooperar | E/S de alto fan-out: APIs, llamadas a DB, recuperaciones de red |
threading | Concurrencia real a nivel de SO para llamadas bloqueantes; modelo mental simple por llamada | GIL todavía serializa el bytecode de Python; cuesta más memoria por trabajador | Puente de una o dos bibliotecas bloqueantes en una aplicación async |
multiprocessing | Ejecución real de CPU en paralelo a través de núcleos | Costo de inicio de proceso pesado, sin memoria compartida, sobrecarga de serialización | Trabajo limitado por CPU: análisis, compresión, lotes numéricos |
asyncio.to_thread / run_in_executor | Mantiene el bucle receptivo mientras una llamada bloqueante se ejecuta en otro lugar | Añade un salto de pool de hilos; todavía no es paralelismo de CPU real | Llamadas bloqueantes aisladas dentro de una base de código mayormente async |
await mientras otra cosa espera, no de la ejecución simultánea.TaskGroup es la herramienta que realmente cancela a los hermanos en caso de fallo.TaskGroup).async def sin descargarlo a un hilo o proceso no cambia cuánto tiempo tarda y puede empeorar todo el bucle al bloquearlo.El programador que mantiene una cola de corrutinas y callbacks listas para ejecutarse, ejecuta una a la vez y cambia a la siguiente cuando la actual espera algo que aún no ha terminado.
No. Es concurrente en un solo hilo: el código de una corrutina se ejecuta en cualquier instante, y el progreso en otras ocurre intercalando en los puntos await, no ejecutándose simultáneamente.
Una corrutina es un objeto de función pausado que no hace nada hasta que se espera o se programa. Una Task envuelve una corrutina para que el bucle pueda ejecutarla y rastrearla independientemente del llamador. Un Future es el primitivo de nivel inferior que representa un resultado eventual sobre el cual se construyen las Tasks.
Cuando una corrutina llega a un await en algo que aún no está listo, se suspende en ese punto y devuelve el control al bucle, que luego ejecuta cualquier otra corrutina lista o callback que esté a continuación en su cola.
El bucle posee la programación del hilo mientras se ejecuta; un segundo bucle en el mismo hilo no tendría forma de intercalarse con el primero, por lo que asyncio impone un bucle activo por hilo y asyncio.run no se anidará dentro de un bucle ya en ejecución.
gather ejecuta awaitables concurrentemente y, por defecto, deja que los demás sigan ejecutándose en segundo plano si uno lanza una excepción. TaskGroup vincula la vida de cada hijo a su bloque async with y cancela activamente a los hijos restantes cuando uno falla, proporcionando garantías de concurrencia estructurada más sólidas.
Significa ejecutar una operación síncrona dentro de una corrutina que nunca cede (un bucle limitado por CPU, una llamada bloqueante a archivo o base de datos), lo que ocupa el bucle por completo y detiene todas las demás corrutinas programadas en él durante toda la duración de esa llamada.
Cuando la operación es proporcionada por una biblioteca que no tiene una versión async (un controlador de base de datos bloqueante, un cálculo limitado por CPU, una API síncrona heredada), to_thread la saca del hilo del bucle para que el bucle siga sirviendo a otras corrutinas mientras se ejecuta.
Sí; dentro de un solo bucle, el bytecode de una corrutina se ejecuta a la vez independientemente del estado del GIL, porque el modelo cooperativo de asyncio ya serializa la ejecución; incluso en la compilación CPython con hilos libres (experimental desde 3.13, continuando en 3.14), ese contrato de programación para un solo bucle no cambia.
Es una implementación de bucle de eventos de reemplazo directo respaldada por libuv, que ofrece menor latencia y mayor rendimiento para cargas de trabajo con muchas E/S que el bucle predeterminado de Python puro, sin cambiar el modelo de programación corrutina/Task/await.
Habilita el modo de depuración de asyncio (PYTHONASYNCIODEBUG=1 o asyncio.run(main(), debug=True)), que expone callbacks lentos, corrutinas nunca esperadas y otros problemas a nivel de programación que un traceback normal no mostraría.
Solo si el cuello de botella es el tiempo dedicado a esperar E/S con alta concurrencia; el trabajo limitado por CPU no gana nada solo con asyncio y necesita multiprocessing o una descarga a hilo/proceso independientemente de si el código circundante es async.
Porque el bucle no puede prevenir una corrutina a mitad de ejecución de la misma manera que un programador del sistema operativo previene un hilo: solo recupera el control en un await, por lo que una función que nunca cede bloquea todo el bucle durante toda su duración, no solo su propia unidad de trabajo lógica.
TaskGroup en profundidadto_thread y run_in_executor en la prácticaVersiones de Stack: Esta página fue escrita para Python 3.14.0 (estable 3.14, mantenimiento 3.13), FastAPI 0.115+, Django 5.2, Flask 3.1, Pydantic 2, PyTorch 2.6+, pandas 2.2+, Polars 1.x, ruff 0.9+, y uv 0.6+.
Revisado por Chris St. John·Última actualización: 19 jul 2026