8.5. Таймаути та скасування¶
Скасування — це термін asyncio для операції «зупинити виконання цієї корутини, підняти всередині неї виключення, щоб вона могла прибрати за собою, та видалити її зі планувальника». Таймаути — найпоширеніша причина скасування чогось; ручний виклик task.cancel() — інша причина.
8.5.1. Скасування задачі¶
Виклик Task.cancel планує підняття asyncio.CancelledError всередині корутини, що виконується, на її наступному await. Корутина може проігнорувати скасування (перехопити виключення й продовжити виконання) або прийняти його — зазвичай вибирають прийняти після виконання коду очищення:
async def network_worker(stream):
try:
while True:
data = await stream.read(64)
# ...
finally:
stream.close()
Проста конструкція finally є найчистішим шаблоном: очищення виконується незалежно від того, чи корутина завершилась нормально, підняла стороннє виключення, чи була скасована. CancelledError поширюється назад через finally, цикл бачить, що задача завершена, а той, хто викликав await task, отримує скасування.
Явне перехоплення CancelledError також допустиме, коли застосунок хоче виконати з ним щось конкретне — записати в журнал, акуратно передати ресурс тощо. Правило таке: підняти його знову після завершення очищення. Поглинання CancelledError залишає корутину живою, коли її викликач попросив зупинитись, — що майже завжди є помилкою:
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. Таймаути з wait_for¶
asyncio.wait_for() обгортає awaitable-об’єкт у дедлайн. Якщо awaitable завершується в межах таймауту, повертається його результат. Якщо ні — awaitable скасовується, а викликач отримує asyncio.TimeoutError
try:
frame = await asyncio.wait_for(grab_frame(), timeout=2)
except asyncio.TimeoutError:
print("camera took too long")
Аргумент seconds приймає число з плаваючою комою. Для дедлайнів у мілісекундах asyncio.wait_for_ms() приймає ціле число мілісекунд — розширення MicroPython, що узгоджується з таймінговими засобами мікропрограми з роздільністю до мілісекунди.
Внутрішньо wait_for робить рівно те, що й ручне скасування: коли дедлайн спливає, він викликає cancel() на загорнутій задачі, CancelledError підіймається всередині корутини, виконуються блоки finally, і після завершення очищення виключення перетворюється на TimeoutError для викликача.
Це означає, що корутина, яка перехоплює та ігнорує CancelledError, зведе нанівець таймаут — дедлайн минув, але корутина відмовилась зупинятись, і wait_for не може примусити її. Попереднє правило застосовується і тут: перехоплюйте CancelledError лише для очищення, після чого піднімайте знову.
8.5.3. Скасування через gather¶
Скасування поширюється вниз через gather(). Якщо задача, що очікує виклику gather, скасовується, кожен awaitable, що ще виконується всередині gather, також скасовується — кожен отримує можливість прибрати за собою через власні блоки finally, перш ніж скасування піднімається до викликача.
У поєднанні з таймаутами це стандартний спосіб встановити дедлайн для групи операцій:
await asyncio.wait_for(
asyncio.gather(a(), b(), c()),
timeout=5,
)
Або всі підоперації завершуються протягом п’яти секунд, або всі скасовуються разом.
8.5.4. Завершення роботи застосунку¶
Скасування — також спосіб коректного завершення реального застосунку. Шаблон однаковий для всіх скриптів: main захоплює дескриптори для довготривалих фонових задач, виконує свою основну роботу, потім скасовує кожен дескриптор і очікує його у блоці 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 — це прийом, що не дає gather повторно підняти CancelledError, який кожна дочірня задача збирається повернути, тож власна причина завершення застосунку — те, що snapshot_loop підняла або не підняла — є тим, що спливає з main.