Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

asyncio.create_task executed even without any awaits in the program

This is a followup question to What does asyncio.create_task() do?

There, as well in other places, asyncio.create_task(c) is described as "immediately" running the coroutine c, when compared to simply calling the coroutine, which is then only executed when it is awaited.

It makes sense if you interpret "immediately" as "without having to await it", but in fact a created task is not executed until we run some await (possibly for other coroutines) (in the original question, slow_coro started being executed only when we await fast_coro).

However, if we do not run any await at all, the tasks are still executed (only one step, not to completion) at the end of the program:

import asyncio

async def counter_loop(x, n):
    for i in range(1, n + 1):
        print(f"Counter {x}: {i}")
        await asyncio.sleep(0.5)
    return f"Finished {x} in {n}"


async def main():
    slow_task = asyncio.create_task(counter_loop("Slow", 4))
    fast_coro = asyncio.create_task(counter_loop("Fast", 2))
    print("Created tasks")
    for _ in range(1000):
        pass
    print("main ended")

asyncio.run(main())
print("program ended")

the output is

Created tasks
main ended
Counter Slow: 1
Counter Fast: 1
program ended

I am curious: why are the two created tasks executed at all if there was no await being run anywhere?

like image 320
user118967 Avatar asked Dec 08 '25 08:12

user118967


2 Answers

Using create_task does add it too the eventloop. Even after leaving main, they are still there and active. You need to actively cancel them before leaving main or set an await barrier at the start of the async function.

The loop is not aware that the code will soon end and will progress just as planned:

Note cancelled tasks will still run once sequentially up to the first await barrier (see: base_events.py executed from asycio.run_forevever)

Awaiting a task allows for the await... code to execute further (when possible)

like image 160
Daraan Avatar answered Dec 09 '25 21:12

Daraan


Let's try to put it in these words: the event loop enters execution with one task to be performed: the main(). When "main()" is complete, there are other two tasks ready to be processed - so before returning to main() caller, those are executed up the next await statement inside each one. (either an await or an async for or async with).

For the next loop iteration, as the main task is "over", the loop cancels the remaining tasks, and shuts down. This happens because what signals the loop that the "main" task is over is a callback that is set when loop.run_until_complete (called by asyncio.run) is done: it is this callback that signals that the loop should stop. But the callback itself will be executed only on the next loop iteration, after the main co-routine is done. And the loop iteration, even though gets a mark that the asyncio loop should stop there, will only actually shutdown after running once over all pending tasks - this implies in advancing each created task to the next await point.

This is done by throwing asyncio.CancelledError into the tasks code, at the await statement. So if you have a try/except/finally clause encompassing the await, you can still clean-up your task before the loop ends.

All of that is not documented in "English" - it is rather the current code behavior in the asyncio implementation. One have to follow the code at the asyncio/base_events.py file to understand it.

like image 40
jsbueno Avatar answered Dec 09 '25 21:12

jsbueno