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?
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)
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With