In Python, we often see asyncio
, but many (including myself) may not fully understand the mechanism and purpose behind it. This article is based on Gao Tian’s YouTube video on asyncio, and it summarizes the inner workings of asyncio
.
Please support the original creator of this content: 码农高天
Before We Begin #
Although
asyncio
allows you to handle multiple tasks “simultaneously,” it is still fundamentally single-threaded and single-process.
Therefore, we can say:
- There’s no context switching overhead when handling multiple tasks in
asyncio
. - All tasks are queued and executed in the same thread.
- The so-called “concurrency” is achieved by switching between coroutines during I/O idle time.
How asyncio
Works
#
- When the program starts, it creates an event loop.
- When an
async
function is called, it creates a coroutine object, which is then wrapped into a task and registered to the event loop. - The event loop continuously polls these tasks and executes whichever is ready.
- When a task reaches an
await
(e.g., waiting for I/O or sleeping), it voluntarily yields control, allowing the event loop to execute other tasks. - Once the awaited operation completes, the task resumes execution until the next
await
or it finishes.
What is a Coroutine? #
async def main():
print('hello')
await asyncio.sleep(1)
print('world')
core = main()
asyncio.run(core)
Any function defined with async def
is a coroutine function. When invoked, it returns a coroutine object.
You can enter asynchronous mode using asyncio.run()
. This function does two things:
- It gives control to the event loop.
- It turns the given coroutine into a task and runs it within the event loop.
Multiple Tasks with Async #
import asyncio
import time
async def say_after(delay, what):
await asyncio.sleep(delay)
print(what)
async def main():
print(f"started at {time.strftime('%X')}")
await say_after(1, 'hello')
await say_after(2, 'world')
print(f"finished at {time.strftime('%X')}")
asyncio.run(main())
When you run a coroutine with await
, the following happens:
- The coroutine after
await
is wrapped into a task and registered with the event loop. - For example, when
main
encountersawait say_after(1, 'hello')
, it tells the event loop thatmain
needs to wait forsay_after
to complete. - It then yields control to the event loop.
- The return value of the awaited coroutine is preserved.
Execution Trace #
Output:
started at 18:08:35
hello
world
finished at 18:08:38
You can see a 3-second delay — this is because the two say_after
coroutines were not registered at the same time. Here’s what happened:
main
is registered as a task.print()
runs first.say_after(1, 'hello')
is registered as a task, andmain
yields control.say_after(1, 'hello')
is picked by the event loop.sleep
is registered as a task, andsay_after
yields control.- All tasks are now waiting; event loop pauses.
sleep
completes,say_after
resumes, printshello
.say_after
finishes and yields control.- Only
main
is left and resumes. - It encounters
await say_after(2, 'world')
, and the cycle repeats. - Finally,
print(finished at ...)
is executed.
Note: The event loop cannot forcefully take control. It must wait for a task to await
or complete.
create_task()
#
How can we register coroutines as tasks earlier? Python provides create_task()
, which registers the coroutine as a task without waiting. It splits the responsibility of await
.
import asyncio
import time
async def say_after(delay, what):
await asyncio.sleep(delay)
print(what)
async def main():
task1 = asyncio.create_task(say_after(1, 'hello'))
task2 = asyncio.create_task(say_after(2, 'world'))
print(f"started at {time.strftime('%X')}")
await task1
await task2
print(f"finished at {time.strftime('%X')}")
asyncio.run(main())
Execution flow:
-
main()
starts → registerstask1
andtask2
-
Both tasks begin
say_after
, but hitawait asyncio.sleep(...)
and immediately yield control. -
The event loop waits for the first to complete:
- task1 wakes after 1 second
- task2 after 2 seconds
-
After 1 second → task1 resumes and prints “hello”
-
After another second → task2 resumes and prints “world”
-
When both tasks are done,
main()
continues to print finish time.
Await Multiple Tasks in One Line: gather
#
You can use gather()
to await multiple tasks at once:
await asyncio.gather(task1, task2)
This waits for all tasks to complete before continuing. It returns a list of results, in the same order as the tasks.
You can also pass coroutines directly into gather()
, and it will register them as tasks automatically:
import asyncio
import time
async def say_after(delay, what):
await asyncio.sleep(delay)
print(what)
async def main():
print(f"started at {time.strftime('%X')}")
await asyncio.gather(say_after(1, 'hello'), say_after(2, 'world'))
print(f"finished at {time.strftime('%X')}")
asyncio.run(main())
Output:
started at 18:37:29
hello
world
finished at 18:37:31
That’s all for this article! Hopefully, it helped you understand how asyncio
works behind the scenes :D