Traditional synchronous programming executes tasks one after another, which can lead to wasted time when waiting for I/O operations. Python addresses this with asynchronous programming, a powerful paradigm that enables efficient concurrency using async, await, and the asyncio library.
Synchronous vs Asynchronous Execution
In synchronous programming, each task blocks the execution until it is complete:import time
def task(name):
print(f"Task {name} started")
time.sleep(2)
print(f"Task {name} finished")
task("A")
task("B")
Here, Task B starts only after Task A finishes. This approach is simple but inefficient for I/O-bound operations.
In contrast, asynchronous programming allows tasks to run concurrently without waiting for each other to complete, especially when tasks spend time waiting for external resources.
What is async and await?
Python introduces async and await keywords to define and manage asynchronous functions.- async def is used to define a coroutine.
- await is used to pause execution until an asynchronous operation completes.
A coroutine is a special function that can be paused and resumed, enabling non-blocking execution.
Basic Example of Async Function
import asyncio
async def task(name):
print(f"Task {name} started")
await asyncio.sleep(2)
print(f"Task {name} finished")
async def main():
await task("A")
await task("B")
asyncio.run(main())
Output:
Task A started
Task A finished
Task B started
Task B finished
Although this uses async functions, tasks still run sequentially because each await waits for completion before moving to the next.
Running Tasks Concurrently
To achieve true concurrency, we use asyncio.create_task() or asyncio.gather().import asyncio
async def task(name):
print(f"Task {name} started")
await asyncio.sleep(2)
print(f"Task {name} finished")
async def main():
t1 = asyncio.create_task(task("A"))
t2 = asyncio.create_task(task("B"))
await t1
await t2
asyncio.run(main())
Output (approximate timing: ~2 seconds, not 4):
Task A started
Task B started
Task A finished
Task B finished
This demonstrates concurrent execution.
The Role of asyncio
The asyncio module provides the infrastructure for writing asynchronous programs. It includes:- Event loop: The core that schedules and runs tasks
- Coroutines: Defined using async def
- Tasks: Wrappers around coroutines
- Futures: Low-level objects representing pending results
The event loop continuously checks for tasks that are ready to run and executes them efficiently.
Using asyncio.gather()
A cleaner way to run multiple tasks concurrently is using asyncio.gather():import asyncio
async def task(name):
print(f"Task {name} started")
await asyncio.sleep(2)
print(f"Task {name} finished")
async def main():
await asyncio.gather(
task("A"),
task("B"),
task("C")
)
asyncio.run(main())
Output:
Task A started
Task B started
Task C started
Task A finished
Task B finished
Task C finished
This runs all tasks concurrently and waits for all of them to complete.
Async vs Multithreading
Although both async programming and multithreading handle concurrency, they work differently.Async programming uses a single thread and an event loop, making it lightweight and efficient for I/O-bound tasks. It avoids the overhead of thread creation and context switching.
Multithreading, on the other hand, uses multiple threads and is subject to the Global Interpreter Lock (GIL), which limits CPU-bound parallelism.
When to Use Async Programming
Async programming is ideal when dealing with tasks that involve waiting, such as:- API calls
- Database queries
- File I/O
- Web scraping
- Real-time applications (chat apps, streaming)
It is not suitable for CPU-bound tasks, where multiprocessing is a better choice.
Example: Simulating API Calls
import asyncio
async def fetch_data(id):
print(f"Fetching data for {id}")
await asyncio.sleep(2)
return f"Data {id}"
async def main():
results = await asyncio.gather(
fetch_data(1),
fetch_data(2),
fetch_data(3)
)
print(results)
asyncio.run(main())
Output:
Fetching data for 1
Fetching data for 2
Fetching data for 3
['Data 1', 'Data 2', 'Data 3']
This simulates multiple API calls happening concurrently.
Error Handling in Async Code
You can handle exceptions in async code using try-except just like synchronous code:import asyncio
async def risky_task():
await asyncio.sleep(1)
raise ValueError("Something went wrong")
async def main():
try:
await risky_task()
except Exception as e:
print(f"Caught error: {e}")
asyncio.run(main())
Output:
Caught error: Something went wrong
Async functions must be called within an event loop, typically using asyncio.run(). Also, blocking operations (like time.sleep) should be avoided inside async code, as they block the entire event loop. Instead, use non-blocking equivalents like asyncio.sleep().
Async programming is widely used in frameworks like FastAPI and in high-performance systems where handling thousands of concurrent connections is required. It is particularly useful in microservices, real-time dashboards, and scalable backend systems.
By leveraging the event loop and coroutines, developers can build highly scalable and responsive applications.
Join the discussion