8.5. Timeouts e cancelamento¶
O cancelamento é a designação usada pelo asyncio para «parar esta coroutine, lançar uma exceção dentro dela para que possa fazer a limpeza, e removê-la do escalonador». Os timeouts são a razão mais comum para cancelar algo; o task.cancel() manual é a outra razão.
8.5.1. Cancelar uma tarefa¶
Chamar Task.cancel agenda o lançamento de asyncio.CancelledError dentro da coroutine em execução no próximo await. A coroutine pode ignorar o cancelamento (capturar a exceção e continuar a executar) ou respeitá-lo – a escolha habitual é respeitá-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 corre quer a coroutine tenha saído normalmente, lançado uma exceção não relacionada, ou tenha sido cancelada. O CancelledError propaga-se de volta através do finally, o loop constata que a tarefa terminou, e o chamador de await task vê o cancelamento.
Capturar CancelledError explicitamente também é válido quando a aplicação precisa de fazer algo específico com isso – registar, libertar o recurso de forma limpa, etc. A regra é: relançá-lo após a limpeza. Engolir CancelledError mantém a coroutine ativa quando o seu chamador pediu para parar, o que é quase sempre um erro:
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 aguardável num prazo. Se o aguardável terminar dentro do timeout, o seu resultado é devolvido. Se não terminar, o aguardável é 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 em milissegundos, asyncio.wait_for_ms() recebe um número inteiro de milissegundos – uma extensão do MicroPython que se alinha com os mecanismos de temporização em milissegundos do firmware.
Internamente, wait_for faz exatamente o que o cancelamento manual faria: quando o prazo expira, chama cancel() na tarefa envolvida, CancelledError é lançado dentro da coroutine, as cláusulas finally executam, e assim que a limpeza termina, a exceção é traduzida para TimeoutError para o chamador.
Isto significa que uma coroutine que captura e ignora CancelledError irá derrotar o timeout – o prazo expirou, mas a coroutine recusou-se a parar, e wait_for não pode forçá-la. A regra anterior aplica-se aqui também: capture CancelledError apenas para executar a limpeza, depois relance.
8.5.3. Cancelamento através do gather¶
O cancelamento propaga-se para baixo através de gather(). Se a tarefa que está a aguardar uma chamada gather for cancelada, todos os aguardáveis ainda em execução dentro do gather também são cancelados – cada um tem a oportunidade de fazer a limpeza através das suas próprias cláusulas finally antes de o cancelamento chegar ao chamador.
Combinado com timeouts, esta é a forma padrão de impor um prazo a um grupo de operações:
await asyncio.wait_for(
asyncio.gather(a(), b(), c()),
timeout=5,
)
Ou todas as sub-operações terminam dentro de cinco segundos, ou todas são canceladas em conjunto.
8.5.4. Encerrar uma aplicação¶
O cancelamento é também a forma como uma aplicação real para de forma limpa. O padrão é consistente nos scripts: main captura os identificadores para as tarefas de fundo de longa duração que iniciou, executa o seu trabalho de nível superior, depois cancela cada identificador e aguarda-o num 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 a razão de saída da própria aplicação – o que quer que snapshot_loop tenha ou não lançado – é o que emerge de main.