corrutinas

Para crealas, async def. Para esperarlas, await. Cuando se crea, cuando se llama a una función definida con async def, obtenemos la corrutina, que no es más que un objeto con la definición del cómputo que queremos realizar, pero éste no se realiza. Para ejecutarlas, tenemos que llamarlas dentro del bucle de eventos de asyncio. El bucle se crea con asyncio.run(coroutine()), y para ejecutarlas dentro simplemente las llamamos con await. Tenemos tres formas de ejecutar corrutinas:

  • Pasándolas a asyncio.run(…)
  • Cuando ya estamos dentro de una corrutina, podemos ejecutar otras con el await.
  • Usando Tasks
import asyncio

async def foo(param):
    ....
    return ...

asyncio.run(foo(param_value))

Si nos fijamos, las dos primeras no nos permite ejecutar código de forma concurrente, puesto que nos obligan a esperar al resultado de su ejecución. Incluso al ejecutar asyncio.run(...), nos quedaremos esperando hasta que ese bloque de código acabe antes de que se ejecute la siguiente línea, no se transfiere el control fuera de la ejecución del bucle de eventos cuando dentro de él se libera el GIL.

Tareas

Una tarea es un objeto que ejecuta de forma independiente una corrutina. Por lo tanto, para crear una necesitamos la corrutina que vaya a envolverse en la tarea. La forma más común de crearlas es con asyncio.create_task.

Nos permite hacer fire and forget, sin tener que esperar (await) a la finalización de la corrutina. Pero también podemos hacer await task hasta que termine la tarea.

Hay que guardar una referencia a la tarea para evitar que el GC la borre en caso de que no esperemos a su finalización. En la documentación se presenta una forma sencilla de hacerlo:

background_tasks = set()

for i in range(10):
    task = asyncio.create_task(some_coro(param=i))
    background_tasks.add(task)
    task.add_done_callback(background_tasks.discard)

Si se lanza una excepción, se cancela la tarea.