23

I was reading asyncio documentation for task cancel and I came across this -

To cancel a running Task use the cancel() method. Calling it will cause the Task to throw a CancelledError exception into the wrapped coroutine. If a coroutine is awaiting on a Future object during cancellation, the Future object will be cancelled.

cancelled() can be used to check if the Task was cancelled. The method returns True if the wrapped coroutine did not suppress the CancelledError exception and was actually cancelled.

I have a few questions here -

  • Is wrapped coroutine the coroutine in which cancel is called? Let's take an example here -

    async def wrapped_coroutine():
        for task in asyncio.Task.all_tasks():
            task.cancel()
    

    So wrapped_coroutine() is the wrapped coroutine where task will throw an exception?

  • When will this exception be thrown? And where?

  • What does suppress the exception mean here? Does it mean this -

    async def wrapped_coroutine():
        for task in asyncio.Task.all_tasks():
            task.cancel()
            try:
                await task
            except asyncio.CancelledError:
                print("Task cancelled")
    

    If not, please provide an example on how to suppress this exception.

And an unrelated(it's related to cancelling tasks), how do I retrieve exceptions out of these tasks when I'm cancelling these so I don't see this -

Task exception was never retrieved future:

Is it before task.cancel() or in try before await task (in the above example) ?

Sushant
  • 3,499
  • 3
  • 17
  • 34
  • Isn't the example in the documentation very clear on this? https://docs.python.org/3/library/asyncio-task.html#asyncio.Task.cancel – Grismar May 09 '19 at 05:37
  • 2
    @Grismar I could not get a clear understanding – Sushant May 09 '19 at 05:43
  • Ok, I hope the explanation I provide below is more clear, but please ask further questions if that doesn't clear it up. – Grismar May 09 '19 at 06:26
  • 2
    "Wrapped" refers to the fact that each Task takes ownership of a coroutine, and in a sense "wraps" it. (When you pass a coroutine to a task, you're no longer allowed to await it directly.) So, wrapped coroutine is the coroutine executed by the task that got cancelled - i.e. not the caller of `task.cancel()`, but its **receiver**. In that coroutine the `await` it is suspended on will resume with a `CancelledError`. – user4815162342 May 09 '19 at 09:10

1 Answers1

22

Looking at the code in the example given in the documentation https://docs.python.org/3/library/asyncio-task.html#asyncio.Task.cancel:

async def cancel_me():
    print('cancel_me(): before sleep')

    try:
        # Wait for 1 hour
        await asyncio.sleep(3600)
    except asyncio.CancelledError:
        print('cancel_me(): cancel sleep')
        raise
    finally:
        print('cancel_me(): after sleep')

async def main():
    # Create a "cancel_me" Task
    task = asyncio.create_task(cancel_me())

    # Wait for 1 second
    await asyncio.sleep(1)

    task.cancel()
    try:
        await task
    except asyncio.CancelledError:
        print("main(): cancel_me is cancelled now")

asyncio.run(main())

Answering your questions:

  • "Is the wrapped coroutine the coroutine in which cancel is called?"
    No, the wrapped coroutine here is cancel_me(); .cancel() is called in main().
  • "When will this exception be thrown? And where?"
    This exception is thrown after task.cancel() is called. It is thrown inside the coroutine, where it is caught in the example, and it is then re-raised to be thrown and caught in the awaiting routine.
  • "What does suppress the exception mean here?"
    If cancel_me() would not have re-raised the exception after catching it. As the documentation of cancelled() states: "The Task is cancelled when the cancellation was requested with cancel() and the wrapped coroutine propagated the CancelledError exception thrown into it." https://docs.python.org/3/library/asyncio-task.html#asyncio.Task.cancelled
Grismar
  • 27,561
  • 4
  • 31
  • 54
  • What if there's a lot of logic in `cancel_me()`? Putting too broad of `try except` is always a bad idea right? – Sushant May 09 '19 at 06:33
  • 1
    Note that `asyncio` does not solve the exact same problem as `multiprocessing` which allows you to launch tasks running in parallel. `asyncio` allows you to run tasks asynchronously, but they still share computing resources. So, the exact moment in "a lot of logic" would have to coincide with some point in the code where control would be returned to the routine. – Grismar May 09 '19 at 06:38
  • And how do retrieve I task exceptions when cancelling them? – Sushant May 09 '19 at 07:02
  • @ThatBird If you need a task's result (be it a value or an exception), you `await` it, and catch exceptions in the usual way (with a `try/await task/except`). If you only need to check if the task is done, you can use `if task.done(): ...`. – user4815162342 May 09 '19 at 11:16
  • @ThatBird exceptions raised in the task will be caught where you `await` them, note the `try .. except` around the await in the example. That applies to `asyncio.CancelledError` exceptions as well as any other exceptions raised in your code. If you mean any other exceptions that you catch in the code itself, just re-raise them and catch them where you await the routine. – Grismar May 10 '19 at 03:48
  • 1
    Why do we need to await on the task after we call `cancel()` on it? What's the point? – CMCDragonkai Apr 29 '21 at 02:58
  • 1
    See here, @CMCDragonkai https://docs.python.org/3/library/asyncio-task.html#asyncio.Task.cancel - calling `.cancel()` still allows the task to wrap up nicely, and that's what's being awaited. – Grismar Jun 16 '21 at 22:42
  • In the example above, if `main()` itself is running in a task, and `main()` itself gets cancelled while doing `await task`, how can `main()` differentiate between `main()` being cancelled (somebody calling `task_running_main.cancel()`) and `cancel_me` being cancelled (by `main()` calling `task.cancel()`)? – lanzz Dec 27 '22 at 19:03
  • 1
    @lanzz `task.cancelled()` is `True` if `task` was cancelled, but `False` in the unlikely (for this snippet) event that the `try...catch` has caught cancellation of the `main()` task – Danny Jan 10 '23 at 18:13