asyncio
: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:
- A partir de la versión 3.5, para definir una corrutina usamos
async def
. - No está solamente limitado a trabajar con operaciones limitadas por IO, sino que permite trabajar también con multi-hilo y multi-proceso.
- Se aprovecha del hecho de que el runtime de Python libera el GIL cuando cede el control al SO en operaciones de IO, y no recupera hasta que el SO le envía la señal correspondiente. Esta señal de fin de la operación de IO hace que la operación que se ejecuta como respuesta (bien como callback bien código secuencial tras el await) se ponga en el bucle de eventos para ejecutarse cuando el runtime considere.
En resumen, la concurrencia se consigue gracias a la liberación del GIL en acciones de IO, y al sistema de notificación del SO en los sockets, que es no bloqueante, cuando se ha escrito todo lo que se tenía que recibir en el socket.
Nos aporta abstracciones (corrutinas y Tasks) sobre los sockets no bloqueantes que nos simplifican el trabajo. No necesitaremos ya trabajar con selectores para registrar y estar atentos a eventos de EventPoll2.
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.
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