8.15. Armadilhas#

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

8.15.1. Esquecer do await#

Chamar uma função async def retorna um objeto corrotina. Isso não executa o corpo da função. Para de fato executá-lo, a corrotina precisa ser awaited ou encapsulada em uma 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 bug é silencioso – o objeto corrotina é criado, descartado e nunca executado. A aplicação prossegue como se tudo tivesse funcionado. O MicroPython às vezes registra um aviso de que uma corrotina nunca foi aguardada; às vezes não. Audite por awaits ausentes em cada ponto de chamada que se pareça com uma chamada de função.

8.15.2. Laços apertados sem await#

Uma corrotina que executa em um laço e nunca faz await monopoliza o event loop. Nenhuma outra tarefa avança até que o laço termine ou ceda o controle:

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

A correção é um yield dentro do laço – geralmente await asyncio.sleep_ms(0) – para que outras tarefas prontas tenham a chance de executar. Trabalho pesado de computação também pertence a esse formato: um laço de processamento de imagem que executa por centenas de milissegundos por iteração deve ceder o controle ao menos uma vez por iteração para que o restante do programa não trave.

8.15.3. Engolir o CancelledError#

A página sobre cancelamento já cobriu isso em detalhes. Repetimos aqui porque é a causa mais comum de “minha aplicação não desliga”: uma corrotina captura asyncio.CancelledError para fins de limpeza e esquece de relançá-lo. A tarefa continua a executar; o chamador que pediu o cancelamento fica pendurado para sempre esperando que ela termine. Sempre relance após a limpeza, ou use um bloco try/finally em vez de um except explícito.

8.15.4. Mutar estado compartilhado entre awaits#

O escalonamento cooperativo garante que uma corrotina tem a CPU só para si entre awaits, mas assim que ela faz await, outra corrotina pode executar. Se duas corrotinas modificam a mesma estrutura de dados em passos que incluem await, suas operações podem se intercalar 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 o estado que é apenas mutado entre awaits dentro de uma única corrotina, nenhuma sincronização é necessária. Para o estado mutado entre awaits e acessado por mais de uma corrotina, envolva a seção crítica em um Lock.

8.15.5. await em nível de módulo#

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

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

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

8.15.6. Múltiplas chamadas a asyncio.run#

O MicroPython tem um event loop. Chamar asyncio.run() duas vezes seguidas – uma para configuração, outra para o trabalho principal – ainda usa o mesmo loop. Chamá-lo de dentro de uma corrotina em execução é um erro: o loop já está em execução. Ambos os casos surgem com mais frequência quando um script cresce organicamente e o autor tenta estendê-lo adicionando mais chamadas a run() em vez de incorporar o novo trabalho ao main existente.

8.15.7. Uso de Event a partir de uma interrupção#

asyncio.Event.set() só pode ser chamado com segurança de dentro do event loop. Chamá-lo a partir de um manipulador de interrupção de GPIO é um risco de corrupção. Para acordar uma tarefa a partir de uma interrupção, use ThreadSafeFlag em vez disso – a página sobre ele cobre o formato.

8.15.8. Chamadas síncronas longas#

Uma corrotina pode aguardar as próprias primitivas de espera do asyncio; qualquer outra coisa que ela chama executa de forma síncrona e bloqueia o loop até retornar. Um time.sleep() bloqueante de 200 ms, uma escrita no cartão SD que leva 80 ms para ser descarregada, uma grande compressão JPEG, uma chamada a csi.CSI.snapshot() – cada uma delas retém o event loop por toda a sua duração. A correção depende da chamada:

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

  • Para csi.CSI.snapshot: use o invólucro de snapshot assíncrono 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 blocos que fazem await entre as iterações.

O asyncio não pode tornar uma chamada síncrona não bloqueante. Ele só pode permitir que outras corrotinas executem enquanto algo mais está sendo aguardado.