pythonasync-awaittaskpython-asynciocancellation

What is the right way to await cancelling an asyncio task?


The docs for cancel make it sound like you should usually propagate CancelledError exceptions:

Therefore, unlike Future.cancel(), Task.cancel() does not guarantee that the Task will be cancelled, although suppressing cancellation completely is not common and is actively discouraged. Should the coroutine nevertheless decide to suppress the cancellation, it needs to call Task.uncancel() in addition to catching the exception.

However, neither of the methods for detecting cancellation are awaitable: cancelling which tells you if cancelling is in progress, and cancelled tells you if cancellation is done. So the obvious way to wait for cancellation is this:

foo_task.cancel()
try:
    await foo_task
except asyncio.CancelledError:
    pass

There are lots of examples of this online even on SO. But the docs warn you asyncio machinery will "misbehave" if you do this:

The asyncio components that enable structured concurrency, like asyncio.TaskGroup and asyncio.timeout(), are implemented using cancellation internally and might misbehave if a coroutine swallows asyncio.CancelledError

Now you might be wondering why you would wait to block until a task is fully cancelled. The problem is the asyncio event loop only creates weak references to tasks, so if as your class is shutting down (e.g. due to a cleanup method or __aexit__) and you don't await every task you spawn, you might tear down the only strong reference while the task is still running, and then python will yell at you:

ERROR base_events.py:1771: Task was destroyed but it is pending!

So it seems to avoid the error I am specifically being forced into doing the thing I'm not supposed to do :P The only alternative seems to be weird unpythonic hackery like stuffing every task I make in a global set and awaiting them all at the end of the run.


Solution

  • You have a cleanup problem. The with statement is generally used to solve cleanup.

    Use asyncio.TaskGroup:

    async with asyncio.TaskGroup() as tg:
        tg.create_task(some_coro(...)).cancel()