8.4. Gather¶
El capítulo anterior mostró cómo asyncio.create_task() programa una corrutina y no la espera. La operación complementaria – ejecutar varios awaitables de forma concurrente y esperar a todos ellos – es asyncio.gather().
8.4.1. La forma básica¶
import asyncio
async def fetch(name, delay_ms):
await asyncio.sleep_ms(delay_ms)
return name
async def main():
results = await asyncio.gather(
fetch("a", 100),
fetch("b", 200),
fetch("c", 50),
)
print(results)
asyncio.run(main())
Salida:
['a', 'b', 'c']
Dos cosas a tener en cuenta. Primero, la lista de resultados está en el orden de los argumentos de gather, no en el orden en que las corrutinas terminaron – "c" retornó primero pero sigue siendo la tercera entrada. Segundo, la llamada tomó 200 ms en total, no 350 ms: las tres esperas se ejecutaron de forma concurrente, y gather() retorna en cuanto la más lenta se completa.
Ambos hechos provienen de la misma fuente. gather() envuelve cada argumento que no sea ya una tarea (las corrutinas del ejemplo), las programa en el bucle, suspende la corrutina que llama hasta que todas terminan, y luego retorna sus resultados en el orden original.
8.4.2. Cuándo recurrir a ella¶
Donde sea que la aplicación tenga N operaciones awaitable y quiera los resultados de todas ellas antes de continuar. Ejemplos típicos:
Emitir varias solicitudes de red en paralelo y esperar todas las respuestas.
Leer de varios sensores de forma concurrente antes de procesar el resultado combinado.
Unir varias tareas auxiliares de corta duración al final de una fase de la aplicación.
No es la herramienta adecuada para tareas en segundo plano de larga duración que la aplicación inicia y deja en marcha durante toda la vida del programa – esas siguen siendo trabajo de create_task(). gather() es para el patrón de fan-out / fan-in: repartir el trabajo, hacerlo de forma concurrente, reunirlo.
8.4.3. Excepciones en el grupo¶
Si alguno de los awaitables agrupados lanza una excepción, el comportamiento predeterminado es volver a lanzar la excepción fuera de la llamada a gather. Los hermanos que aún no han terminado se cancelan en segundo plano.
Eso suele ser lo que una aplicación quiere – uno de los N trabajos paralelos falló, por lo que la operación combinada ha fallado, así que deja de gastar tiempo en el resto. A veces la aplicación quiere lo contrario: dejar que cada awaitable termine (o falle) de forma independiente e inspeccionar los resultados después. Pasa return_exceptions=True para eso:
results = await asyncio.gather(
fetch_or_fail("ok"),
fetch_or_fail("bad"),
return_exceptions=True,
)
# results == ["ok-value", OSError(...)]
Cada entrada de la lista retornada es ahora o bien un valor de retorno normal o la excepción que lanzó el awaitable correspondiente. Quien llama comprueba isinstance(r, Exception) para distinguirlas.
8.4.4. Cancelación¶
Cancelar el propio gather – cancelando la tarea que lo estaba esperando – cancela todos los awaitables que aún se ejecutan dentro de él. La página de tiempos de espera y cancelación cubre en detalle cómo se propaga la cancelación a través de la cadena de llamadas.