8.5. Timeouts e cancelamento¶
Cancelamento é o nome que o asyncio dá para “pare de executar esta corrotina, lance uma exceção dentro dela para que ela tenha a chance de fazer a limpeza e remova-a do agendador”. Os timeouts são o motivo mais comum para cancelar algo; o task.cancel() manual é o outro.
8.5.1. Cancelando uma tarefa¶
Chamar Task.cancel agenda o lançamento de asyncio.CancelledError dentro da corrotina em execução em seu próximo await. A corrotina é livre para ignorar o cancelamento (capturar a exceção e continuar executando) ou para honrá-lo – a escolha habitual é honrá-lo após executar o código de limpeza:
async def network_worker(stream):
try:
while True:
data = await stream.read(64)
# ...
finally:
stream.close()
Uma cláusula finally simples é o padrão mais limpo: a limpeza é executada quer a corrotina tenha saído normalmente, lançado uma exceção não relacionada ou sido cancelada. O CancelledError se propaga de volta através do finally, o laço vê que a tarefa terminou, e o chamador de await task vê o cancelamento.
Capturar CancelledError explicitamente também é adequado quando a aplicação quer fazer algo específico com ele – registrá-lo, repassar o recurso de forma limpa etc. A regra é: relance-o depois que a limpeza for executada. Engolir o CancelledError mantém a corrotina viva quando seu chamador pediu para ela parar, o que quase sempre é um bug:
async def worker():
try:
await long_running_thing()
except asyncio.CancelledError:
log("worker cancelled, cleaning up")
close_resources()
raise # << this line is the important one
8.5.2. Timeouts com wait_for¶
asyncio.wait_for() envolve um awaitable em um prazo. Se o awaitable terminar dentro do timeout, seu resultado é retornado. Caso contrário, o awaitable é cancelado e o chamador recebe asyncio.TimeoutError
try:
frame = await asyncio.wait_for(grab_frame(), timeout=2)
except asyncio.TimeoutError:
print("camera took too long")
O argumento de segundos aceita um float. Para prazos no formato de milissegundos, asyncio.wait_for_ms() recebe uma contagem inteira de milissegundos – uma extensão do MicroPython que se alinha com os controles de temporização do firmware na granularidade de milissegundos.
Internamente, wait_for faz exatamente o que o cancelamento manual faria: quando o prazo expira, ele chama cancel() na tarefa envolvida, o CancelledError é lançado dentro da corrotina, as cláusulas finally são executadas e, uma vez concluída a limpeza, a exceção é traduzida em um TimeoutError para o chamador.
Isso significa que uma corrotina que captura e ignora o CancelledError vai derrotar o timeout – o prazo expirou, mas a corrotina se recusou a parar, e o wait_for não pode forçá-la. A regra anterior também se aplica aqui: capture o CancelledError apenas para executar a limpeza e, em seguida, relance-o.
8.5.3. Cancelamento através do gather¶
O cancelamento se propaga para baixo através de gather(). Se a tarefa que está aguardando uma chamada de gather for cancelada, todos os awaitables ainda em execução dentro do gather também serão cancelados – cada um tem a chance de fazer a limpeza por meio de suas próprias cláusulas finally antes que o cancelamento suba até o chamador.
Combinado com timeouts, esta é a maneira padrão de colocar um prazo em um grupo de operações:
await asyncio.wait_for(
asyncio.gather(a(), b(), c()),
timeout=5,
)
Ou todas as suboperações terminam dentro de cinco segundos, ou todas elas são canceladas juntas.
8.5.4. Desligando uma aplicação¶
O cancelamento também é como uma aplicação real para de forma limpa. O padrão é consistente entre os scripts: main captura os handles das tarefas de segundo plano de longa duração que iniciou, executa seu trabalho de nível superior e, em seguida, cancela cada handle e o aguarda em um bloco finally
async def main():
sender = asyncio.create_task(uplink())
watcher = asyncio.create_task(button_watcher())
try:
await snapshot_loop()
finally:
sender.cancel()
watcher.cancel()
await asyncio.gather(sender, watcher,
return_exceptions=True)
O return_exceptions=True é o truque que impede o gather de relançar o CancelledError que cada tarefa filha está prestes a entregar, de modo que o próprio motivo de saída da aplicação – o que quer que snapshot_loop tenha ou não lançado – é o que aflora de main.