8.15. Errores comunes

Los mismos patrones que hacen agradable a asyncio – sin apropiación, awaits explícitos – le dan su propio conjunto de situaciones que muerden. Esta página es el catálogo de las que surgen con la suficiente frecuencia como para que valga la pena conocerlas.

8.15.1. Olvidar el await

Llamar a una función async def devuelve un objeto corrutina. No ejecuta el cuerpo de la función. Para ejecutarlo realmente, la corrutina tiene que ser objeto de await o envolverse en una tarea:

async def main():
    send_request()              # bug: returns the coroutine, does nothing
    await send_request()        # right: run it to completion
    asyncio.create_task(send_request())  # right: run it concurrently

El error es silencioso – el objeto corrutina se crea, se descarta y nunca se ejecuta. La aplicación continúa como si todo hubiera funcionado. MicroPython a veces registrará una advertencia de que una corrutina nunca fue esperada; otras veces no lo hará. Audita la falta de await en cada punto de llamada que parezca una llamada a función.

8.15.2. Bucles cerrados sin await

Una corrutina que se ejecuta en un bucle y nunca hace await monopoliza el bucle de eventos. Ninguna otra tarea avanza hasta que el bucle termina o cede:

async def counter():
    n = 0
    while True:
        n += 1               # bug: starves the loop

La solución es un yield dentro del bucle – normalmente await asyncio.sleep_ms(0) – para que otras tareas listas tengan la oportunidad de ejecutarse. El trabajo intensivo en cómputo también pertenece a esa forma: un bucle de procesamiento de imagen que se ejecuta durante cientos de milisegundos por iteración debería ceder al menos una vez por iteración para que el resto del programa no se atasque.

8.15.3. Tragarse CancelledError

La página de cancelación ya cubrió esto en detalle. Lo repetimos aquí porque es la causa más común de «mi aplicación no se apaga»: una corrutina captura asyncio.CancelledError con fines de limpieza y olvida volver a lanzarla. La tarea sigue ejecutándose; el llamador que solicitó la cancelación queda colgado para siempre esperando a que finalice. Vuelve a lanzar siempre tras la limpieza, o usa un bloque try/finally en lugar de un except explícito.

8.15.4. Mutar estado compartido a través de awaits

La planificación cooperativa garantiza que una corrutina tenga la CPU para sí misma entre awaits, pero en cuanto hace await, otra corrutina puede ejecutarse. Si dos corrutinas modifican la misma estructura de datos en pasos que incluyen await, sus operaciones pueden intercalarse de maneras que corrompan la estructura:

# bug: two tasks running do_work simultaneously can
# interleave around the await and corrupt items
async def do_work():
    n = len(items)
    await asyncio.sleep_ms(0)
    items.append(some_work(n))

Para el estado que se muta únicamente entre awaits dentro de una sola corrutina, no se necesita sincronización. Para el estado que se muta a través de awaits y al que se accede desde más de una corrutina, envuelve la sección crítica en un Lock.

8.15.5. await a nivel de módulo

await solo es válido dentro de un cuerpo async def. Escribirlo a nivel de módulo – fuera de cualquier corrutina – es un error de sintaxis:

# bug: not inside an async def
result = await fetch()

La solución es poner el trabajo en una corrutina y llamarla desde el punto de entrada asyncio.run() del programa.

8.15.6. Múltiples llamadas a asyncio.run

MicroPython tiene un bucle de eventos. Llamar a asyncio.run() dos veces seguidas – una para la configuración, otra para el trabajo principal – sigue usando el mismo bucle. Llamarlo desde dentro de una corrutina en ejecución es un error: el bucle ya está en marcha. Ambos casos surgen con mayor frecuencia cuando un script crece de forma orgánica y el autor intenta ampliarlo añadiendo más llamadas a run() en lugar de integrar el nuevo trabajo en el main existente.

8.15.7. Uso de Event desde una interrupción

asyncio.Event.set() solo es seguro de llamar desde dentro del bucle de eventos. Llamarlo desde un manejador de interrupciones de GPIO es un riesgo de corrupción. Para despertar una tarea desde una interrupción, usa en su lugar ThreadSafeFlag – la página dedicada cubre el patrón.

8.15.8. Llamadas síncronas largas

Una corrutina puede esperar las propias primitivas de espera de asyncio; cualquier otra cosa que llame se ejecuta de forma síncrona y bloquea el bucle hasta que retorna. Un time.sleep() bloqueante de 200 ms, una escritura en tarjeta SD que tarda 80 ms en volcarse, una compresión JPEG grande, una llamada a csi.CSI.snapshot() – cada una de ellas retiene el bucle de eventos durante toda su duración. La solución depende de la llamada:

  • Para time.sleep: reemplázalo por await asyncio.sleep o await asyncio.sleep_ms.

  • Para csi.CSI.snapshot: usa el envoltorio de captura asíncrona que construye la página de captura.

  • Para cómputo largo (procesamiento de imagen, codificación JPEG): asume el coste o divide el trabajo en fragmentos que hagan await entre iteraciones.

Asyncio no puede convertir una llamada síncrona en no bloqueante. Solo puede dejar que otras corrutinas se ejecuten mientras algo más está esperando.