8.5. Limity czasu i anulowanie¶
Anulowanie to nazwa nadana przez asyncio dla „zatrzymaj wykonywanie tej korutyny, zgłoś wewnątrz niej wyjątek, aby miała szansę posprzątać, i usuń ją z harmonogramu”. Limity czasu są najczęstszym powodem anulowania czegoś; ręczne task.cancel() jest tym drugim.
8.5.1. Anulowanie zadania¶
Wywołanie Task.cancel planuje zgłoszenie asyncio.CancelledError wewnątrz działającej korutyny przy jej następnym await. Korutyna może swobodnie zignorować anulowanie (przechwycić wyjątek i kontynuować działanie) lub uszanować je – zwykłym wyborem jest uszanowanie go po wykonaniu kodu sprzątającego:
async def network_worker(stream):
try:
while True:
data = await stream.read(64)
# ...
finally:
stream.close()
Goła klauzula finally jest najczystszym wzorcem: sprzątanie wykonuje się niezależnie od tego, czy korutyna zakończyła się normalnie, zgłosiła niezwiązany wyjątek, czy została anulowana. CancelledError propaguje się z powrotem w górę przez finally, pętla widzi, że zadanie jest zakończone, a wywołujący await task widzi anulowanie.
Jawne przechwytywanie CancelledError jest również w porządku, gdy aplikacja chce zrobić z nim coś konkretnego – zapisać do logu, czysto przekazać zasób itp. Zasada brzmi: zgłoś go ponownie po wykonaniu sprzątania. Pochłonięcie CancelledError utrzymuje korutynę przy życiu, gdy jej wywołujący poprosił ją o zatrzymanie, co prawie zawsze jest błędem:
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. Limity czasu za pomocą wait_for¶
asyncio.wait_for() opakowuje obiekt awaitable terminem. Jeśli obiekt awaitable zakończy się w obrębie limitu czasu, jego wynik jest zwracany. Jeśli nie, obiekt awaitable zostaje anulowany, a wywołujący otrzymuje asyncio.TimeoutError
try:
frame = await asyncio.wait_for(grab_frame(), timeout=2)
except asyncio.TimeoutError:
print("camera took too long")
Argument sekund przyjmuje wartość zmiennoprzecinkową. Dla terminów w skali milisekund asyncio.wait_for_ms() przyjmuje całkowitą liczbę milisekund – rozszerzenie MicroPython, które współgra z pokrętłami czasowymi oprogramowania układowego o ziarnistości milisekundowej.
Wewnętrznie wait_for robi dokładnie to, co zrobiłoby ręczne anulowanie: gdy termin wygasa, wywołuje cancel() na opakowanym zadaniu, CancelledError jest zgłaszany wewnątrz korutyny, klauzule finally wykonują się, a po zakończeniu sprzątania wyjątek jest tłumaczony na TimeoutError dla wywołującego.
Oznacza to, że korutyna, która przechwytuje i ignoruje CancelledError, pokona limit czasu – termin wygasł, ale korutyna odmówiła zatrzymania, a wait_for nie może jej do tego zmusić. Wcześniejsza zasada ma tu również zastosowanie: przechwytuj CancelledError tylko po to, aby wykonać sprzątanie, a następnie zgłoś go ponownie.
8.5.3. Anulowanie poprzez gather¶
Anulowanie propaguje się w dół przez gather(). Jeśli zadanie oczekujące na wywołanie gather zostanie anulowane, każdy obiekt awaitable nadal działający w obrębie gather również zostaje anulowany – każdy z nich dostaje szansę na posprzątanie poprzez własne klauzule finally, zanim anulowanie zwinie się do wywołującego.
W połączeniu z limitami czasu jest to standardowy sposób nałożenia terminu na grupę operacji:
await asyncio.wait_for(
asyncio.gather(a(), b(), c()),
timeout=5,
)
Albo każda podoperacja zakończy się w obrębie pięciu sekund, albo wszystkie zostaną anulowane razem.
8.5.4. Zamykanie aplikacji¶
Anulowanie jest również sposobem, w jaki prawdziwa aplikacja zatrzymuje się czysto. Wzorzec jest spójny we wszystkich skryptach: main przechwytuje uchwyty długotrwałych zadań w tle, które uruchomiło, wykonuje swoją pracę najwyższego poziomu, a następnie anuluje każdy uchwyt i oczekuje na niego w bloku 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)
return_exceptions=True to sztuczka, która powstrzymuje gather przed ponownym zgłaszaniem CancelledError, który każde zadanie potomne ma zamiar dostarczyć, dzięki czemu to własny powód zakończenia aplikacji – niezależnie od tego, co snapshot_loop zgłosił lub nie zgłosił – jest tym, co wypływa z main.