8.1. Concurrencia cooperativa

El modelo de planificación de asyncio es cooperativo, no expropiativo. Esta distinción es el modelo mental más importante sobre el que se construye el resto de esta sección, así que conviene fijarlo antes de que aparezca cualquier código.

8.1.1. Expropiativo frente a cooperativo

Un planificador expropiativo —del tipo que usa un sistema operativo de escritorio para mantener muchos programas ejecutándose a la vez— puede pausar en cualquier momento la porción de código que se está ejecutando y cambiar a otra. El código en ejecución no tiene que hacer nada especial; el planificador lo interrumpe. Eso hace que la planificación expropiativa sea muy flexible (ninguna porción de código puede dejar sin recursos a las demás por ser lenta), pero también significa que cualquier variable compartida debe protegerse cuidadosamente, porque el cambio podría producirse en cualquier punto —incluso a mitad de escribir un valor, o a mitad de leer una lista.

Un planificador cooperativo solo puede cambiar entre porciones de código en los puntos en los que la porción que se está ejecutando cede explícitamente el control de vuelta. En asyncio, esos puntos son cada await y cada llamada a una corrutina que cede internamente (lo más habitual, asyncio.sleep()). Entre dos awaits, la corrutina en ejecución tiene la CPU para ella sola.

De ahí se derivan dos consecuencias:

  • Una corrutina que nunca hace await nunca se pausa. Si una corrutina permanece en un bucle cerrado sin ningún await dentro, monopoliza el planificador y nada más avanza. La solución es hacer await asyncio.sleep_ms(0) (o alguna otra llamada de espera) en un punto razonable del bucle.

  • El estado compartido es seguro entre awaits. Dos corrutinas no pueden intercalarse a mitad de una operación que no tenga ningún await en ella. El tipo de corrupción que surge cuando la expropiación cae en medio de una actualización de varios pasos —una porción de código leyendo un valor mientras otra está a mitad de cambiarlo— sencillamente no puede ocurrir aquí. Sigue siendo necesaria la coordinación entre corrutinas cuando varias de ellas tienen que compartir un recurso a lo largo de awaits, pero el problema de la intercalación a mitad de una línea no se aplica.

8.1.2. Las tres capas

Todo script de asyncio se construye a partir de las mismas tres capas. Las dos páginas siguientes las tratan en detalle; estas son las etiquetas que conviene tener presentes al leerlas.

  • Corrutinas —funciones declaradas con async def, cada una una unidad de trabajo autónoma que hace await donde corresponde. La Introducción a Python presentó las palabras clave async/await; en asyncio son la forma en que una corrutina cede de vuelta al planificador.

  • Tareas —un envoltorio que asyncio.create_task() coloca alrededor de una corrutina para planificarla de forma concurrente con la actual. La aplicación normalmente crea un puñado de tareas para los trabajos de larga duración (el bucle de capturas, el cliente de red, el lector de UART, …).

  • El bucle de eventos —el motor subyacente que lleva la cuenta de qué corrutinas están esperando y cuáles están listas para ejecutarse, cambiando entre tareas en cada await. La aplicación no escribe el bucle; le entrega una corrutina de nivel superior a asyncio.run() y el bucle dirige todo a partir de ahí.

Cuando la aplicación se describe de ese modo —como un pequeño conjunto de corrutinas compuestas por un bucle de eventos— la concurrencia se convierte en una propiedad de la forma del programa, no en algo que la aplicación tenga que gestionar paso a paso.