pythonasync-awaitpython-asyncio

Python async sleep question ordering of execution


Hello I have a quesiton while I'm working through some tutorial on asyncio as given below:

import asyncio
async def task_coroutine():
    print('start executing task')
    await asyncio.sleep(1.0)
    print('done waiting in task')
    raise Exception('error in task')
    return 99

async def main():
    print('main started')
    task = asyncio.create_task(task_coroutine())
    await asyncio.sleep(1.1)
    print('done waiting in main')
    try:
        value = task.result()
    except Exception as e:
        print(e)
    print('end of main')
asyncio.run(main())

as I expected, the result is

main started
start executing task
done waiting in task
done waiting in main
error in task
end of main

because when the task suspend, the main is still sleeping and it switches back to task to throw.

I have been playing around a bit by changing the sleep time of main to 0.1 (1.1 before) and the result is also as I expected

main started
start executing task
done waiting in main
Result is not set.
end of main

since the main trying to collect result of task before it's done so it throws "Result is not set"

However, when I set the sleep time of main to 1.0000000001, it gives:

main started
start executing task
done waiting in main
Result is not set.
end of main
done waiting in task

Task exception was never retrieved
future: <Task finished name='Task-2' coro=<task_coroutine() done, defined at /Users/user/Library/Application Support/JetBrains/PyCharm2024.1/scratches/scratch.py:3> exception=Exception('error in task')>
Traceback (most recent call last):
  File "/Users/charlielao/Library/Application Support/JetBrains/PyCharm2024.1/scratches/scratch.py", line 7, in task_coroutine
    raise Exception('error in task')
Exception: error in task

I don't quite understand why it prints "done waiting in task" at the end after main finishes where it didn't in the 0.1 case, why is there explicit error in this case, what was the exact ordering of events?


Solution

  • The long and short of it is that asyncio.run() continues to run tasks after your main() coroutine finishes. This is to give other tasks a chance to terminate gracefully, rather than to be left in some unknown state. Since the difference in durations between your sleeps is so small, this means the await statement in your child tasks finishes normally. As a result, the child task is instead able to finish by raising your custom exception.

    Detail

    When the function asyncio.run() finishes running the coroutine that it was passed, it performs some post clean up steps. One of these steps is to call cancel() on each pending task and then run the event loop until each of those tasks has finished. When a task is cancelled a CancelledError is raised at the next pending await statement of the task. This causes well behaved tasks to terminate quickly.

    Timings

    The difference in the durations of your sleeps is so small, that while the sleep() is still pending when you call task.result(), it is done by the time the event loop gets around to cancelling pending tasks. What this means in practice is that the event loop processes the event that signals the end of the sleep coroutine before it gets a chance to cancel the sleep coroutine. This means a CancelledError cannot be raised by the sleep() coroutine. As there are no further await statements in the task there is no opportunity to raise a CancelledError. Instead the execution of your task continues until it reaches your raise statement. This statement causes your task to terminate with an exception. It is considered a bug if any task terminates with an exception that is not CancelledError, and that exception is never retrieved. As such, the event loop reports this unhandled exception.