8.15. Armadilhas

Os mesmos padrões que tornam o asyncio agradável – sem preempção, awaits explícitos – conferem-lhe o seu próprio conjunto de armadilhas. Esta página é o catálogo das que surgem com frequência suficiente para valer a pena conhecer.

8.15.1. Esquecer o await

Chamar uma função async def devolve um objeto coroutine. O corpo da função não é executado. Para o executar de facto, a coroutine tem de ser chamada com awaitou encapsulada numa tarefa:

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

O erro é silencioso – o objeto coroutine é criado, descartado e nunca executado. A aplicação prossegue como se tudo tivesse funcionado. O MicroPython por vezes regista um aviso indicando que uma coroutine nunca foi aguardada; outras vezes não o faz. Audite os awaits em falta em todos os pontos de chamada que pareçam chamadas a funções.

8.15.2. Ciclos apertados sem await

Uma coroutine que corre em ciclo e nunca faz awaits monopoliza o ciclo de eventos. Nenhuma outra tarefa avança até o ciclo terminar ou ceder:

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

A correção consiste numa cedência dentro do ciclo – geralmente await asyncio.sleep_ms(0) – para que outras tarefas prontas tenham oportunidade de correr. O trabalho computacionalmente intensivo também deve ter esta forma: um ciclo de processamento de imagem que demora centenas de milissegundos por iteração deve ceder pelo menos uma vez por iteração para que o resto do programa não fique bloqueado.

8.15.3. Engolir CancelledError

A página de cancelamento já abordou isto em detalhe. Repete-se aqui porque é a causa mais comum de «a minha aplicação não encerra»: uma coroutine captura asyncio.CancelledError para efeitos de limpeza e esquece-se de a relançar. A tarefa continua a correr; o chamador que pediu o cancelamento fica à espera para sempre que ela termine. Relance sempre após a limpeza, ou use um bloco try/finally em vez de um except explícito.

8.15.4. Mutação de estado partilhado entre awaits

O agendamento cooperativo garante que uma coroutine tem o CPU para si própria entre awaits, mas logo que faz await, outra coroutine pode correr. Se duas coroutines modificarem a mesma estrutura de dados em passos que incluem await, as suas operações podem intercalar-se de formas que corrompem a estrutura:

# 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 estado que é apenas mutado entre awaits dentro de uma coroutine, não é necessária sincronização. Para estado mutado entre awaits e acedido por mais de uma coroutine, encapsule a secção crítica numa Lock.

8.15.5. await ao nível do módulo

await só é válido dentro de um corpo async def. Escrevê-lo ao nível do módulo – fora de qualquer coroutine – é um erro de sintaxe:

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

A correção consiste em colocar o trabalho numa coroutine e chamá-la a partir do ponto de entrada asyncio.run() do programa.

8.15.6. Múltiplas chamadas asyncio.run

O MicroPython tem um ciclo de eventos. Chamar asyncio.run() duas vezes seguidas – uma para a configuração, outra para o trabalho principal – continua a usar o mesmo ciclo. Chamá-lo de dentro de uma coroutine em execução é um erro: o ciclo já está a correr. Ambos os casos surgem mais frequentemente quando um script cresce organicamente e o autor tenta expandi-lo adicionando mais chamadas run() em vez de incorporar o novo trabalho no main existente.

8.15.7. Utilização de Event a partir de uma interrupção

asyncio.Event.set() só é seguro de chamar de dentro do ciclo de eventos. Chamá-lo a partir de um handler de interrupção GPIO é um risco de corrupção. Para acordar uma tarefa a partir de uma interrupção, use ThreadSafeFlag – a página sobre este tema aborda a forma correta.

8.15.8. Chamadas síncronas longas

Uma coroutine pode aguardar as primitivas de espera do asyncio; qualquer outra coisa que chame corre de forma síncrona e bloqueia o ciclo até retornar. Um time.sleep() bloqueante de 200 ms, uma escrita em cartão SD que demora 80 ms a consolidar, uma compressão JPEG grande, uma chamada csi.CSI.snapshot() – cada uma destas retém o ciclo de eventos pela sua duração total. A correção depende da chamada:

  • Para time.sleep: substitua por await asyncio.sleep ou await asyncio.sleep_ms.

  • Para csi.CSI.snapshot: use o wrapper assíncrono de captura que a página de captura constrói.

  • Para computação longa (processamento de imagem, codificação JPEG): aceite o custo ou divida o trabalho em partes que fazem await entre iterações.

O asyncio não pode tornar uma chamada síncrona não-bloqueante. Só pode permitir que outras coroutines corram enquanto outra coisa está em await.