PythonMiddleTechnical

Что такое cancellation в asyncio?

Cancellation — кооперативный запрос задаче завершиться: task.cancel() помечает Task, а CancelledError инжектится в следующей точке await. Очистка делается в finally, и CancelledError нужно повторно пробросить.

Модель cancellation

Отмена в asyncio кооперативная. Task.cancel() не убивает поток или корутину мгновенно — он помечает Task флагом и планирует выброс asyncio.CancelledError в следующей точке await. CPU-bound цикл без await отменить нельзя: пока coroutine не отдаст управление, loop её не трогает.

С Python 3.8 CancelledError наследуется от BaseException, а не Exception — это сделано специально, чтобы except Exception: случайно её не глотал.

Что делать в коде задачи

import asyncio

async def worker():
    try:
        while True:
            await asyncio.sleep(1)
            do_work()
    except asyncio.CancelledError:
        # освобождаем ресурсы синхронно или с await на короткие операции
        await close_connection()
        raise            # ОБЯЗАТЕЛЬНО пробросить, иначе structured concurrency сломается
    finally:
        cleanup_local_state()

async def main():
    task = asyncio.create_task(worker())
    await asyncio.sleep(0.1)
    task.cancel()
    try:
        await task
    except asyncio.CancelledError:
        print("worker cancelled cleanly")

asyncio.run(main())

Как cancellation связан с timeout и TaskGroup

  • asyncio.wait_for(coro, timeout) внутри использует task.cancel() по таймеру.
  • asyncio.timeout() (3.11+) делает то же через context manager и корректно обрабатывает shielded scopes.
  • TaskGroup отменяет оставшиеся задачи, если одна упала с исключением.
  • asyncio.shield(coro) защищает coroutine от внешней отмены — отменяется только внешняя обёртка.

Подводные камни

  • Глотать CancelledError внутри except Exception (на 3.7 это происходит автоматически) — task видится как успешно завершённая, родитель не узнаёт об отмене.
  • Делать долгий blocking I/O в finally — отмена «зависает» пока cleanup не закончится.
  • В 3.11+ повторный cancel() увеличивает cancelling() счётчик; uncancel() нужно вызывать осторожно.
  • Полагаться, что task.cancel() остановит CPU-bound цикл без await — не остановит.

Common mistakes

  • Думать, что cancel убивает поток.
  • Не знать, где возникает CancelledError.
  • Не использовать finally для cleanup.

What the interviewer is testing

  • Объясняет cooperative cancellation.
  • Понимает cleanup и re-raise.
  • Знает связь с timeout/TaskGroup.

Sources

Related topics