asyncio

Índice

:ID: 267597bb-ce29-4c25-9623-e89ff1541050

AsyncIO es una actualización del mecanismo de las corrutinas que había en Python 3.4, donde se usaba el yield from.1 Puntos importantes:

Multi-tarea

Mientras que los SO suelen implementar la multi-tarea preemptiva3, asyncio usa multi-tarea cooperativa, lo que permita mejorar el rendimiento al no realizarse tantos cambios de contexto, sino que somos los programadores los que marcamos los puntos en el flujo del programa en que queremos pasar el control.

Bucle de Eventos

El elemento central es el bucle de eventos. Los eventos en el caso de asyncio se llaman tareas, que no son más que envoltorios alrededor de corrutinas. Cuando una tarea que estaba en pausa porque estaba en medio de la operación de IO se pone en modo resumen, se incluye en la cola de eventos y en cuanto se puede el bucle la despierta para finalizarla.

Hay que hacer hincapié en que Python no tiene un bucle de eventos como sí JavaScript, sino que cuando con AsyncIO vamos a ejecutar corrutinas dicho módulo se encarga de crearlo para ejecutarlas y pararlo cuando termina la ejecución de dichas corrutinas.

APIs no asíncronas

Hay que tener mucho cuidado al trabajar con API o clientes. Por ejemplo, si trabajamos con requests, al ser bloqueante, aunque usemos cualquiera de los mecanismos explicados, hay que ser conscientes que el bucle de eventos se ejecuta en un único hilo, luego si lo que usemos bloquea ese hilo bloquearemos toda la ejecución del proceso en el que se está ejecutando el bucle.

Corrutinas

Tasks

Las tareas se ejecutan nada más las creamos, y podemos esperar a su resultado a posteriori, por lo que con ellas es como podemos ejecutar el código de forma concurrente.

async def bar(param):
    await asyncio.sleep(5)
    return 42

async def foo(param):
    # Se ejectua el código de la tarea nada más se crean.
    task1 = asyncio.create_task(bar(param))
    task2 = asyncio.create_task(bar(param))
    # Automáticamente se ejecuta las siguiente línea.
    do_whatever(1)
    print('forever')
    # Aquí ya bloqueamos esperando a que se hayan acabado.
    result1 = await task1
    result2 = await task2
    return result1 + result2

Futuros

Un futuro es un contenedor de un valor el cuál esperamos tener en algún momento en el futuro. Se parecen a las tareas en que podemos crearlos y luego esperar su valor (la clase Task hereda de Future). De hecho las tareas pueden verse como una mezcla entre corrutinas y futuros. En el diagrama awaitable-hierarchy puede verse la jerarquía de clases. Las tres implementan el dunder _await__. Cualquier clase que implemente este último puede usarse con async/await.

classDiagram
  Awaitable <|-- Coroutine
  Awaitable <|-- Future
  Future <|-- Task

awaitable_hierarchy.png

Notas

1

David Beazley tiene algún vídeo en Youtube muy bueno sobre este mecanismo.

2

En el caso de Linux, para otros sistemas operativos los mecanismos aunque similares son distintos y reciben distinto nombre.

3

El algoritmo más famoso es el Round Robin.