Async for loop en AsyncGenerator

Teniendo un generador asíncrono esperaría poder iterar a través de él de forma asíncrona. Sin embargo, me estoy perdiendo algo o estropeando algo o ambos, ya que al final tengo un ciclo síncrono para el final:

import asyncio async def time_consuming(t): print(f"Going to sleep for {t} seconds") await asyncio.sleep(t) print(f"Slept {t} seconds") return t async def generator(): for i in range(4, 0, -1): yield await time_consuming(i) async def consumer(): async for t in generator(): print(f"Doing something with {t}") if __name__ == '__main__': loop = asyncio.new_event_loop() loop.run_until_complete(consumer()) loop.close() 

Esto tardará unos 12 segundos en ejecutarse y devolver esto:

 Going to sleep for 4 seconds Slept 4 seconds Doing something with 4 Going to sleep for 3 seconds Slept 3 seconds Doing something with 3 Going to sleep for 2 seconds Slept 2 seconds Doing something with 2 Going to sleep for 1 seconds Slept 1 seconds Doing something with 1 

Aunque esperaba que llevara unos 4 segundos correr y devolver algo como esto:

 Going to sleep for 4 seconds Going to sleep for 3 seconds Going to sleep for 2 seconds Going to sleep for 1 seconds Slept 4 seconds Doing something with 4 Slept 3 seconds Doing something with 3 Slept 2 seconds Doing something with 2 Slept 1 seconds Doing something with 1 

¡Un generador asíncrono no significa que ejecutes la iteración simultáneamente! Todo lo que ganas es más lugares para que la coroutine ceda a otras tareas. Los pasos de iteración todavía se ejecutan en serie .

Dicho de otra manera: un iterador asíncrono es útil para un iterador que necesita usar I / O para obtener cada paso de iteración. Piense en un bucle sobre los resultados de un socket web, o líneas en un archivo. Si cada paso next() sobre el iterador requiere esperar a que una fuente de E / S lenta proporcione datos, ese es un buen punto para ceder el control a otra cosa que se haya configurado para ejecutarse simultáneamente.

Si esperaba que cada paso individual de su generador se ejecutara simultáneamente, aún tendría que progtwigr tareas adicionales, explícitamente , con el bucle de eventos.

Luego, puede regresar del generador cuando todas esas tareas adicionales se hayan completado. Si programó sus 4 time_consuming() de time_consuming() como tareas, use asyncio.wait() para esperar a que una o todas las tareas se completen y obtener los resultados de las tareas que se realizan, entonces sí, después de que esté for i in range(...): ciclo está completo, su proceso solo tomaría 4 segundos en total:

 async def generator(): pending = [] for i in range(4, 0, -1): pending.append(asyncio.create_task(time_consuming(i))) while pending: done, pending = await asyncio.wait(pending, return_when=asyncio.FIRST_COMPLETED) for task in done: yield task.result() 

en que punto se convierte la salida

 Going to sleep for 4 seconds Going to sleep for 3 seconds Going to sleep for 2 seconds Going to sleep for 1 seconds Slept 1 seconds Doing something with 1 Slept 2 seconds Doing something with 2 Slept 3 seconds Doing something with 3 Slept 4 seconds Doing something with 4 

Tenga en cuenta que este es el orden inverso de su salida esperada, ya que toma los resultados de la tarea a medida que se completan en lugar de esperar a que se cree la primera tarea. Por lo general, esto es lo que realmente quieres. ¿Por qué esperar 4 segundos cuando ya tienes un resultado listo después de 1?

También puede tener su variante, de una forma, pero simplemente lo codificaría de manera diferente. Luego, solo puede usar asyncio.gather() en las 4 tareas , que progtwign un montón de coroutines para que se ejecuten como tareas concurrentes, y devuelva sus resultados como una lista, después de lo cual puede obtener esos resultados:

 async def generator(): tasks = [] for i in range(4, 0, -1): tasks.append(time_consuming(i)) for res in await asyncio.gather(*tasks): yield res 

pero ahora la salida se convierte en

 Going to sleep for 4 seconds Going to sleep for 3 seconds Going to sleep for 2 seconds Going to sleep for 1 seconds Slept 1 seconds Slept 2 seconds Slept 3 seconds Slept 4 seconds Doing something with 4 Doing something with 3 Doing something with 2 Doing something with 1 

porque no podemos hacer nada más hasta que la tarea más larga, time_consuming(4) , se haya completado, sin embargo, las tareas de ejecución más corta se completen antes de ese punto y ya Slept ... seconds mensaje Slept ... seconds .