La multitarea cooperativa es una forma de alternar tareas que ceden voluntariamente el turno.
Imagina que quieres hacer dos cosas sencillas:
- Hacer parpadear un LED rojo cada 1 segundo.
- Hacer parpadear un LED azul cada 0.2 segundos.
Si intentas hacerlo con lo que hemos aprendido hasta ahora (usando time.sleep), te darás cuenta de que es imposible.
# El enfoque clásico (BLOQUEANTE)
while True:
led_rojo.toggle()
time.sleep(1) # <--- ¡El procesador se congela aquí 1 segundo!
led_azul.toggle() # Esto nunca podrá ir más rápido que el led rojo
time.sleep(0.2)
Cuando el programa llega a time.sleep(1), el microcontrolador se cruza de brazos y no hace absolutamente nada más durante ese segundo. Ni mirar botones, ni leer sensores, ni parpadear el otro LED. A esto se le llama Código Bloqueante.
Para solucionar esto, MicroPython incluye la librería uasyncio (micro asynchronous I/O).
¿Qué es la multitarea cooperativa?
A diferencia de tu ordenador, que tiene varios núcleos y un Sistema Operativo complejo que reparte el tiempo a la fuerza (Multitarea Preferente), en un microcontrolador solemos tener un solo núcleo.
uasyncio implementa una Multitarea Cooperativa. Esto significa que las tareas son “educadas”. En lugar de acaparar la CPU, cada tarea dice: “Voy a esperar un rato, así que cedo el turno para que otra tarea aproveche el procesador mientras tanto”.
El encargado de gestionar estos turnos es el Event Loop (Bucle de Eventos).
Sintaxis: async y await
Para usar uasyncio, necesitamos aprender dos palabras clave nuevas en Python:
async def: Define una Corrutina. Es una función especial que puede pausarse y reanudarse.await: Es el punto donde cedemos el turno. Significa “Espera aquí hasta que termine esto, y mientras tanto, deja que otros trabajen”.
La regla básica
**Adiós time.sleep(), Hola uasyncio.sleep()**
Nunca, bajo ningún concepto, uses time.sleep() dentro de una función asíncrona. Eso detendría todo el sistema. Debes usar await uasyncio.sleep(), que es la versión no bloqueante.
Dos LED a ritmos diferentes
Vamos a resolver el problema del inicio. Tenemos dos tareas independientes (corrutinas) y un planificador que las ejecuta.
import uasyncio
from machine import Pin
# Definimos los LEDs
led_rojo = Pin(14, Pin.OUT)
led_azul = Pin(2, Pin.OUT)
# Corrutina 1: Parpadea lento (1 segundo)
async def parpadear_rojo():
while True:
led_rojo.toggle()
print("Rojo!")
# Pausa no bloqueante de 1s
# Cede el control al Event Loop
await uasyncio.sleep(1)
# Corrutina 2: Parpadea rápido (0.2 segundos)
async def parpadear_azul():
while True:
led_azul.toggle()
# Pausa no bloqueante de 0.2s
await uasyncio.sleep(0.2)
# Función principal que organiza todo
async def main():
print("Iniciando multitarea...")
# Programamos las tareas en el Event Loop
# create_task "lanza" la tarea pero no detiene la ejecución
uasyncio.create_task(parpadear_rojo())
uasyncio.create_task(parpadear_azul())
# Mantenemos el programa principal vivo para siempre
while True:
await uasyncio.sleep(10)
# Arrancamos el motor de uasyncio
try:
uasyncio.run(main())
except KeyboardInterrupt:
print("Programa detenido")
finally:
uasyncio.new_event_loop() # Limpieza opcional
Si ejecutas esto, verás el efecto: los dos LEDs parpadean a su propio ritmo de forma independiente. El LED azul parpadea 5 veces por cada vez que parpadea el rojo.
¿Qué está pasando realmente?
- Rojo se enciende y dice
await uasyncio.sleep(1). “Me voy a dormir 1s, CPU libre”. - El Event Loop mira quién más tiene trabajo. ¡Azul!
- Azul se enciende y dice
await uasyncio.sleep(0.2). “Me voy a dormir 0.2s”. - El Loop espera 0.2s. Despierta a Azul.
- Azul cambia, duerme otra vez…
- …y así hasta que pasa 1 segundo y Rojo despierta.
Esperar eventos: Pulsadores asíncronos
uasyncio no solo sirve para tiempos. También es genial para esperar eventos externos, como pulsar un botón, sin estar preguntando todo el rato (if button.value() == 0).
Aunque se puede hacer con interrupciones (IRQ), hacerlo con asyncio es más seguro porque evitamos los problemas de memoria de las IRQ.
Una estructura típica sería:
async def esperar_boton():
btn = Pin(0, Pin.IN, Pin.PULL_UP)
anterior = 1
while True:
actual = btn.value()
if actual == 0 and anterior == 1:
print("¡Botón pulsado!")
# Aquí podríamos lanzar otra tarea o cambiar una variable global
anterior = actual
# Pequeña espera para "debounce" y para ceder la CPU
await uasyncio.sleep_ms(50)
Para casos avanzados, existen primitivas de sincronización como uasyncio.Event o uasyncio.Lock, que permiten que una tarea “avise” a otra cuando ocurre algo importante.
Consejos para organizar las tareas
Implementar uasyncio requiere un cambio de mentalidad:
- Todo contagia: Si usas
asyncen una función, quien la llame debe usarawait. El asincronismo se propaga hacia arriba hasta llegar almain(). - No bloquees: Cualquier cálculo pesado o espera con
time.sleepdentro de una corrutina congelará TODAS las demás tareas. - Usa
create_task: Para lanzar procesos que deben correr en paralelo (background). - Usa
gather: Si necesitas lanzar 3 tareas y esperar a que las 3 terminen antes de seguir.
El 90% de los proyectos avanzados de IoT (servidores web, MQTT, lectura de sensores continua) se benefician enormemente de esta arquitectura. Una vez que te acostumbras a la multitarea cooperativa, el código secuencial te parecerá cosa del pasado.