8.2. Corrotinas e tasks

Corrotinas são a unidade de trabalho a partir da qual um programa asyncio é construído; tasks são como uma aplicação executa várias corrotinas concorrentemente.

8.2.1. Corrotinas

Uma corrotina é uma função declarada com async def

import asyncio

async def heartbeat(interval_ms):
    while True:
        print("tick")
        await asyncio.sleep_ms(interval_ms)

O corpo se parece com uma função comum, com um ingrediente extra: await. Sempre que a corrotina precisa esperar por algo – um sleep, uma leitura de rede, um evento sendo definido – ela faz await em uma expressão que sabe como suspender a corrotina até que aquilo que ela está esperando esteja pronto. A cada await, a corrotina devolve o controle ao asyncio; o asyncio a retoma a partir do mesmo ponto assim que a operação aguardada for concluída.

O módulo asyncio fornece dois sleeps:

  • asyncio.sleep() – argumento em segundos, aceita um float.

  • asyncio.sleep_ms() – argumento em milissegundos, recebe um int. Uma extensão do MicroPython; geralmente a escolha certa na câmera, porque os ajustes de tempo no firmware têm formato de milissegundos.

Um simples async def não faz nada por si só. Chamar heartbeat(500) não executa o corpo; retorna um objeto corrotina que o asyncio precisa escalonar. A maneira mais simples de escalonar um é asyncio.run()

asyncio.run(heartbeat(500))

asyncio.run() inicia o event loop, escalona a corrotina que recebeu como ponto de entrada de nível superior, conduz o loop até que essa corrotina retorne e então desmonta o loop. Para uma única corrotina, esse é o programa inteiro. Para várias corrotinas, a aplicação recorre a tasks.

8.2.2. Tasks

Uma task é o invólucro do asyncio em volta de uma corrotina que diz escalone isto concorrentemente com a atual e me deixe seguir em frente. asyncio.create_task() cria uma e retorna um objeto Task representando o trabalho escalonado:

task = asyncio.create_task(heartbeat("fast", 100))

A corrotina agora está na agenda do loop; o chamador não esperou por ela. O Task retornado é o identificador que o chamador usa posteriormente para interagir com esse trabalho em execução.

Uma vez que a aplicação tem o identificador, ela pode fazer três coisas com ele:

  • Esperar a task terminar. Um Task é ele próprio aguardável. result = await task suspende a corrotina atual até que a corrotina de task retorne, e então a retoma com o que quer que essa corrotina tenha retornado (ou relança o que quer que ela tenha lançado).

  • Cancelar a task. task.cancel() escalona o lançamento de asyncio.CancelledError dentro da corrotina da task em seu próximo await, dando a ela a chance de executar código de limpeza em um bloco finally. A página sobre timeouts e cancelamento aborda os detalhes.

  • Identificá-la mais tarde. asyncio.current_task() retorna o Task da corrotina que está em execução no momento. A maioria dos scripts nunca a chama; ela aparece em instrumentação e em manipuladores de exceções.

O script não precisa capturar o identificador toda vez. Tasks de fundo descartáveis que a aplicação inicia e deixa em execução podem descartar o valor de retorno – o loop ainda as escalona:

import asyncio

async def heartbeat(name, interval_ms):
    while True:
        print(name)
        await asyncio.sleep_ms(interval_ms)

async def main():
    asyncio.create_task(heartbeat("fast", 100))
    asyncio.create_task(heartbeat("slow", 500))
    await asyncio.sleep(5)

asyncio.run(main())

As duas chamadas create_task escalonam ambos os heartbeats sem esperar por nenhum deles. O controle retorna imediatamente para main, que então faz await em um sleep de cinco segundos. Enquanto ele dorme, as duas tasks de heartbeat progridem; o loop percorre qualquer task que esteja pronta para executar. Após cinco segundos, main retorna, o loop desmonta quaisquer tasks que ainda estejam ativas e asyncio.run() retorna ao chamador.

Capture o identificador sempre que a aplicação realmente precisar de uma das três operações acima. Na prática, isso significa quase sempre, porque encerrar uma aplicação de forma limpa significa cancelar as tasks de fundo que ela gerou – a página de cancelamento aborda esse padrão.

8.2.3. A regra das duas linhas

O programa asyncio mínimo são as duas linhas com que os exemplos acima terminam:

async def main():
    ...

asyncio.run(main())

Todo o resto – as tasks que a aplicação cria, os primitivos com os quais ela as coordena, os streams que ela abre – acontece dentro de main (e dentro das corrotinas que main gera). Quando um script ultrapassa o clássico loop while True: csi0.snapshot() da câmera, a resposta não é chamar asyncio.run() em vários lugares; é incorporar o novo trabalho em main como mais tasks.