Athena Wisdom
Athena Wisdom

Reputation: 6811

Awaiting a asyncio Future after Cancelling it

Looking at the asyncio docs, I came across this example

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())

After task.cancel(), what is the purpose of doing await task? Is this to wait for the future to be finished if it was ever shielded from cancellation?

In other words, why not:

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

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

    task.cancel()

asyncio.run(main())

Upvotes: 1

Views: 922

Answers (1)

Ulisse Bordignon
Ulisse Bordignon

Reputation: 106

From the documentation of cancel() (under asyncio.Task):

This arranges for a CancelledError exception to be thrown into the wrapped coroutine on the next cycle of the event loop.

The coroutine then has a chance to clean up or even deny the request by suppressing the exception with a try … … except CancelledErrorfinally block.

When asyncio.CancelledError is thrown into cancel_me(), execution resumes in the except asyncio.CancelledError block. For the snippet provided with the documentation, it does not in fact make any difference whether cancel_me() is awaited or not after cancelling, because the exception handling block executes synchronously.

On the other hand, if the exception handling block did perform asynchronous operations, the difference would become visible:

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

    try:
        # Wait for 1 hour
        await asyncio.sleep(3600)
    except asyncio.CancelledError:
        await asyncio.sleep(1)
        print('cancel_me(): cancel sleep, this never gets printed')
        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()
    print("main(): cancel_me is cancelled now")

asyncio.run(main())

# Expected output:
#
#     cancel_me(): before sleep
#     main(): cancel_me is cancelled now
#     cancel_me(): after sleep

The last, surprising print takes place because of the following:

  • main() returns after its last print
  • asyncio.run() tries to cancel all pending tasks
  • cancel_me(), albeit already cancelled, is still pending, awaiting on the exception block sleep
  • the finally clause in cancel_me() is executed and the even loop terminates

Also worth noting: given that asyncio.run() throws a CancelledError into all the tasks that are still pending, if cancel_me() had not been cancelled already, the except asyncio.CancelledError block would execute in its entirety.

Upvotes: 2

Related Questions