8.2. Corrotinas e tarefas

As corrotinas são a unidade de trabalho a partir da qual se constrói um programa asyncio; as tarefas são a forma como uma aplicação executa várias corrotinas em simultâneo.

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 parece uma função normal, com um ingrediente extra: await. Onde quer que a corrotina tenha de aguardar por algo – um sleep, uma leitura de rede, um evento a ser definido – ela faz awaitde uma expressão que sabe como suspender a corrotina até que aquilo por que está à espera esteja pronto. Em cada await, a corrotina devolve o controlo ao asyncio; o asyncio retoma-a a partir do mesmo ponto assim que a operação aguardada tiver concluído.

O módulo asyncio inclui dois sleeps:

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

  • asyncio.sleep_ms() – argumento em milissegundos, recebe um int. Uma extensão MicroPython; normalmente a escolha certa na câmara porque os controlos de temporização no firmware são em milissegundos.

Um simples async def por si só não faz nada. Chamar heartbeat(500) não executa o corpo; devolve um objeto corrotina que o asyncio tem de agendar. A forma mais simples de agendar uma é asyncio.run()

asyncio.run(heartbeat(500))

asyncio.run() inicia o ciclo de eventos, agenda a corrotina que lhe foi passada como ponto de entrada de nível superior, conduz o ciclo até essa corrotina terminar e, em seguida, encerra o ciclo. Para uma única corrotina, isso é o programa inteiro. Para várias corrotinas, a aplicação recorre a tarefas.

8.2.2. Tarefas

Uma tarefa é o invólucro asyncio em torno de uma corrotina que diz agendar isto em simultâneo com a atual e deixa-me continuar. asyncio.create_task() cria uma e devolve um objeto Task que representa o trabalho agendado:

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

A corrotina está agora no agendamento do ciclo; quem a chamou não esperou por ela. O Task devolvido é o identificador que o chamador usa depois para interagir com esse trabalho em execução.

Assim que a aplicação tiver o identificador, pode fazer três coisas com ele:

  • Aguardar que a tarefa termine. Um Task é ele próprio aguardável. result = await task suspende a corrotina atual até que a corrotina de task retorne, e depois retoma com o que essa corrotina devolveu (ou volta a lançar o que ela lançou).

  • Cancelar a tarefa. task.cancel() agenda o lançamento de asyncio.CancelledError dentro da corrotina da tarefa no seu próximo await, dando-lhe a oportunidade de executar código de limpeza num bloco finally. A página sobre timeouts e cancelamento cobre os detalhes.

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

O script não tem de capturar o identificador sempre. As tarefas de segundo plano descartáveis que a aplicação inicia e deixa correr podem descartar o valor de retorno – o ciclo continua a agendá-las:

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 agendam ambos os heartbeats sem esperar por nenhum deles. O controlo regressa imediatamente a main, que depois faz awaitde um sleep de cinco segundos. Enquanto dorme, as duas tarefas de heartbeat progridem; o ciclo percorre as tarefas que estão prontas para executar. Após cinco segundos, main retorna, o ciclo encerra quaisquer tarefas ainda ativas, e asyncio.run() devolve o controlo ao chamador.

Capture o identificador sempre que a aplicação efetivamente precisar de uma das três operações acima. Na prática isso significa quase sempre, porque encerrar corretamente uma aplicação implica cancelar as tarefas de segundo plano que ela criou – a página de cancelamento cobre o 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())

Tudo o resto – as tarefas que a aplicação cria, as primitivas com que as coordena, os streams que abre – acontece dentro de main (e dentro das corrotinas que main cria). Quando um script supera o ciclo clássico while True: csi0.snapshot() da câmara, a resposta não é chamar asyncio.run() em vários lugares; é incorporar o novo trabalho em main como mais tarefas.