Как внутри тела таска определить, что его отменяют
Что проверяют
Понимаете ли вы, как устроена отмена в asyncio: это не флаг, который надо опрашивать, а исключение, которое прилетает в точке await. Следом обычно спрашивают, можно ли подавить CancelledError и как отмена сочетается с TaskGroup и asyncio.timeout().
Как отвечать
task.cancel() помечает задачу, и при ближайшей приостановке в корутину выбрасывается asyncio.CancelledError. Внутри тела об отмене узнают именно так:
async def worker():
try:
while True:
await process_next()
except asyncio.CancelledError:
await flush_buffer()
raise
finally:
conn.close()Ключевые детали:
- перебрасывать исключение: после очистки
CancelledErrorпробрасывают дальше. Проглоченная отмена ломаетTaskGroupиasyncio.timeout(), которые на неё рассчитывают; BaseException: с Python 3.8CancelledErrorне наследуется отException, поэтому общийexcept Exceptionего не перехватит, и это правильно;Task.cancelling()(3.11+): число невыполненных запросов на отмену, его можно проверить внутри тела без исключения. Парныйuncancel()уменьшает счётчик, такasyncio.timeout()иTaskGroupотличают свою отмену от внешней;cancel(msg): с 3.9 можно передать причину, она попадёт в исключение;asyncio.shield(): защищает внутреннюю операцию от отмены внешней задачи, например чтобы дописать запись в базу.
У потоков встроенной отмены нет: поток сам проверяет threading.Event, а Future.cancel() из concurrent.futures не остановит уже запущенную функцию.
Где спотыкаются
Проверяют asyncio.current_task().cancelled() внутри тела. Этот метод возвращает True только после того, как задача завершилась отменой, а пока она выполняется, там всегда False.
Ловят CancelledError и не перебрасывают: задача завершается «успешно», и таймауты с группами задач начинают вести себя неправильно.
Пишут CPU-нагруженный цикл без единого await. Отмене негде сработать, пока цикл не закончится, помогает периодический await asyncio.sleep(0) как точка отмены.