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")
Аргумент секунд принимает число с плавающей точкой. Для крайних сроков в миллисекундах 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, которое каждая дочерняя задача вот-вот доставит, так что наружу из main всплывает собственная причина выхода приложения – то, что snapshot_loop возбудил или не возбудил.