Python is often used for building complex applications that need to handle multiple tasks concurrently. There are two main approaches in Python for writing concurrent code:
- Asyncio - An asynchronous, non-blocking approach based on an event loop.
- Synchronous - A sequential, blocking approach where each line of code finishes before moving to the next.
Understanding the difference between these approaches is key to writing efficient, scalable Python programs. In this comprehensive guide, we'll cover:
Whether you're new to concurrency in Python or looking to deepen your knowledge, read on to master the critical skill of managing competing tasks in your Python apps.
Blocking vs Non-Blocking Code
The key difference between synchronous and asyncio Python lies in how they handle blocking operations.
In programming, a block operation pauses the execution of code until some other process finishes. Common examples include:
Blocking code stops all other work in your program until the blocking task completes. This can bottleneck performance.
Non-blocking code uses async programming to avoid pauses. Rather than waiting for blocking tasks to finish, the program can switch to other work while awaiting the results.
Asyncio provides an event loop and async/await syntax so you can write non-blocking concurrent Python code easily.
Let's look at a simple example of blocking vs non-blocking code:
# Synchronous blocking code
import time
start = time.time()
print("Starting")
time.sleep(2) # Pause for 2 seconds (blocking)
print(f"Finished in {time.time() - start} seconds")
This synchronous code blocks/pauses for 2 seconds during
Now let's make it non-blocking with asyncio:
import asyncio
async def sleep_task():
print("Sleep task starting")
await asyncio.sleep(2)
print("Sleep task done")
async def main():
start = time.time()
print("Program starting")
await sleep_task()
print(f"Program finished in {time.time() - start} seconds")
asyncio.run(main())
By using
This example shows the performance difference - asyncio allows concurrent execution where synchronous code would block.
How Asyncio Works in Python
The asyncio module lets you write asynchronous, non-blocking programs using an event loop:
Here is a simplified overview of running asyncio code:
- All async tasks get wrapped in coroutines and submitted to the event loop queue.
- The event loop begins executing tasks and awaits any I/O bound ops like network requests.
- Rather than blocking, the loop switches to other ready tasks while awaiting I/O results.
- Callbacks execute when the awaited I/O finishes to handle results and continue execution.
- The loop manages concurrency by coordinating between tasks and callbacks.
Within this architecture:
Let's see an asyncio example to illustrate the components working together:
import asyncio
import time
async def fetch_data():
print("Starting fetch")
await asyncio.sleep(2) # Pause without blocking
print("Completed fetch - got data!")
return {"data": 1}
async def process(data):
print(f"Processing {data}")
await asyncio.sleep(3)
print("Finished processing")
async def main():
start = time.time()
print("Program begin")
data = await fetch_data() # Await coroutine
await process(data)
print(f"Program complete in {time.time() - start} seconds")
asyncio.run(main())
Output:
Program begin
Starting fetch
Completed fetch - got data!
Processing {'data': 1}
Finished processing
Program complete in 3.00 seconds
This shows how:
Understanding asyncio architecture helps write efficient async programs.
When to Use Asyncio vs Synchronous Python
So when should you use asynchronous asyncio, and when is synchronous code fine?
Use asyncio for I/O bound workloads - Network requests, reading files, user input all benefit from non-blocking async execution.
Use synchronous code for CPU heavy processing - Math operations, data processing, algorithms should run sequentially.
Some guidelines on when to use each approach:
Asyncio is great when:
Synchronous code works well for:
For many programs you'll want a mix of asynchronous I/O coordination with synchronous computational work.
Asyncio in Practice
Let's walk through some practical async coding patterns to see how asyncio can create responsive concurrent programs.
We'll build an async web scraper that fetches multiple URLs concurrently:
import asyncio
import aiohttp
async def fetch(url):
async with aiohttp.ClientSession() as session:
async with session.get(url) as response:
data = await response.text()
return data
async def main():
urls = ["https://example.com", "https://python.org"]
tasks = []
for url in urls:
tasks.append(asyncio.create_task(fetch(url)))
results = await asyncio.gather(*tasks)
for result in results:
print(f"Got {len(result)} bytes from url")
asyncio.run(main())
Key points:
Other common async patterns include:
Practice with these patterns unlocks the true power of asyncio in Python.
Summary
Here are the key takeaways:
Hopefully this overview gives you a comprehensive foundation on using asyncio effectively in Python. Mastering async programming unlocks huge performance potential.
The event loop handles much complexity - our code can focus on application logic while asyncio schedules efficient task execution under the hood.
To dive deeper into concurrency with Python, check out these additional resources: