Understanding Python Asyncio: Core Concepts, Event Loop Mechanics, and Performance Comparison
This article introduces Python's asyncio library, explains the fundamentals of coroutines, tasks, and the event loop, compares synchronous and asynchronous code execution with practical examples, and demonstrates how asynchronous programming can significantly improve performance in I/O‑bound scenarios.
If you have decided to understand Python's asynchronous features, welcome to our "Asyncio How‑to" guide.
Even without knowing the existence of asynchronous paradigms, you can use Python successfully, but if you are interested in the underlying execution model, asyncio is definitely worth exploring.
What is Asynchrony?
In traditional sequential programming, all instructions sent to the interpreter are executed one by one, making output predictable. However, when a script makes requests to multiple servers, one request may take much longer than others, causing the whole script to idle while waiting.
By switching tasks, you can minimize idle time, but for simple scripts without I/O you may not need async code.
All code runs in a single thread, so you cannot have a part of the program running in the background while doing other work.
Getting Started
The basic definitions in asyncio are:
Coroutine – a generator that consumes data without producing it; Python 2.5 introduced syntax to send data into generators. See David Beazley's "A Curious Course on Coroutines and Concurrency" for details.
Task – the coroutine scheduler. The code below shows how the event loop quickly calls the task's _step method, which simply advances the coroutine.
<code>class Task(futures.Future):
def __init__(self, coro, loop=None):
super().__init__(loop=loop)
...
def _step(self):
try:
result = next(self._coro)
except StopIteration as exc:
self.set_result(exc.value)
except BaseException as exc:
self.set_exception(exc)
raise
else:
self._loop.call_soon(self._step)
</code>Event Loop – the central executor of asyncio.
All asynchronous code runs in a single thread, and the event loop orchestrates task execution.
The diagram below illustrates the process:
Key points:
The message loop runs in a thread.
Tasks are fetched from a queue.
Each task performs the next step in its coroutine.
Calling another coroutine with await triggers a context switch, suspending the current coroutine and loading the called one.
If a coroutine reaches a blocking operation (I/O, sleep ), it suspends and returns control to the loop, which proceeds with the next task.
When all tasks finish, the loop returns to the first task.
Async vs Sync Code Comparison
We compare two Python scripts that are identical except for using time.sleep (synchronous) versus asyncio.sleep (asynchronous).
Using synchronous sleep :
<code>import asyncio
import time
from datetime import datetime
async def custom_sleep():
print('SLEEP', datetime.now())
time.sleep(1)
async def factorial(name, number):
f = 1
for i in range(2, number+1):
print('Task {}: Compute factorial({})'.format(name, i))
await custom_sleep()
f *= i
print('Task {}: factorial({}) is {}'.format(name, number, f))
start = time.time()
loop = asyncio.get_event_loop()
tasks = [
asyncio.ensure_future(factorial("A", 3)),
asyncio.ensure_future(factorial("B", 4)),
]
loop.run_until_complete(asyncio.wait(tasks))
loop.close()
end = time.time()
print('Total time:', end - start)
</code>Output shows the total execution time.
Using asynchronous sleep :
<code>import asyncio
import time
from datetime import datetime
async def custom_sleep():
print('SLEEP {}'.format(datetime.now()))
await asyncio.sleep(1)
async def factorial(name, number):
f = 1
for i in range(2, number+1):
print('Task {}: Compute factorial({})'.format(name, i))
await custom_sleep()
f *= i
print('Task {}: factorial({}) is {}'.format(name, number, f))
start = time.time()
loop = asyncio.get_event_loop()
tasks = [
asyncio.ensure_future(factorial("A", 3)),
asyncio.ensure_future(factorial("B", 4)),
]
loop.run_until_complete(asyncio.wait(tasks))
loop.close()
end = time.time()
print('Total time:', end - start)
</code>From the output we see that the asynchronous version runs about two seconds faster. When using await asyncio.sleep(1) , control returns to the main event loop, allowing other queued tasks to run.
With the standard sleep , the current thread blocks and does nothing else, although the interpreter can still manage other threads, which is a separate topic.
Reasons to Prefer Asynchronous Programming
Many large‑scale products, such as Facebook's React Native and RocksDB, rely heavily on async patterns. Twitter handles billions of daily requests using asynchronous programming. Refactoring code to use async can make systems faster, so it's worth trying.
Recommended Resources
Scan the QR code below to get free Python learning materials, including e‑books, tutorials, project templates, and source code.
Recommended reading:
These 15 Python libraries you must know
A detailed Python self‑learning roadmap
8 books to master Python web scraping
Why Python can be a game‑changer for graduates
Python Programming Learning Circle
A global community of Chinese Python developers offering technical articles, columns, original video tutorials, and problem sets. Topics include web full‑stack development, web scraping, data analysis, natural language processing, image processing, machine learning, automated testing, DevOps automation, and big data.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.