Introduction
Asynchronous programming has become essential for building high-performance applications. Whether you’re building web servers, data pipelines, or real-time systems, understanding concurrency and parallelism helps you design efficient systems that can handle thousands of simultaneous operations.
This comprehensive guide covers asynchronous programming fundamentals, the difference between concurrency and parallelism, and practical implementation patterns in Python and JavaScript. You’ll learn when to use async programming and how to avoid common pitfalls.
Async programming is particularly essential for I/O-bound applications like web servers, API clients, and database operations. By understanding these concepts, you can dramatically improve your application’s throughput and responsiveness.
Understanding Concurrency vs Parallelism
The Key Distinction
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Concurrency vs Parallelism โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ Concurrency โ โ
โ โ โข Handling multiple tasks at once โ โ
โ โ โข Not necessarily running simultaneously โ โ
โ โ โข Task switching creates illusion of progress โ โ
โ โ โข Great for I/O-bound tasks โ โ
โ โ โ โ
โ โ Example: Restaurant with one waiter serving multiple tables โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ Parallelism โ โ
โ โ โข Running multiple tasks simultaneously โ โ
โ โ โข Requires multiple CPU cores โ โ
โ โ โข True simultaneous execution โ โ
โ โ โข Great for CPU-bound tasks โ โ
โ โ โ โ
โ โ Example: Multiple chefs cooking different dishes โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
When to Use Each
| Scenario | Approach | Example |
|---|---|---|
| Network requests | Concurrency | Fetching multiple APIs |
| CPU computation | Parallelism | Image processing |
| Database queries | Concurrency | Multiple queries |
| File I/O | Concurrency | Reading multiple files |
| Web servers | Both | Handling many requests |
Python Asyncio
Getting Started
import asyncio
async def fetch_data(url: str, delay: float = 1.0) -> dict:
"""Simulate fetching data from URL."""
await asyncio.sleep(delay) # Simulate network call
return {"url": url, "data": f"Data from {url}"}
async def main():
"""Main async function."""
# Sequential execution (slow)
print("Sequential:")
result1 = await fetch_data("http://api1.com")
result2 = await fetch_data("http://api2.com")
result3 = await fetch_data("http://api3.com")
print(f"Results: {[r['url'] for r in [result1, result2, result3]]}")
# Concurrent execution with gather (fast)
print("\nConcurrent:")
results = await asyncio.gather(
fetch_data("http://api1.com"),
fetch_data("http://api2.com"),
fetch_data("http://api3.com")
)
print(f"Results: {[r['url'] for r in results]}")
# Run the async main
asyncio.run(main())
Async Context Managers
class AsyncDatabaseConnection:
"""Async database connection context manager."""
async def __aenter__(self):
"""Connect to database."""
await asyncio.sleep(0.1) # Simulate connection
print("Connected to database")
return self
async def __aexit__(self, exc_type, exc_val, exc_tb):
"""Close database connection."""
await asyncio.sleep(0.05) # Simulate cleanup
print("Disconnected from database")
return False # Don't suppress exceptions
async def main():
async with AsyncDatabaseConnection() as conn:
# Perform database operations
result = await conn.query("SELECT * FROM users")
print(f"Query result: {result}")
# Using async context manager
async def database_example():
async with AsyncDatabaseConnection() as db:
await db.execute("INSERT INTO logs VALUES ('test')")
results = await db.query("SELECT * FROM logs")
Task Management
async def task_with_timeout():
"""Execute task with timeout."""
try:
result = await asyncio.wait_for(
fetch_data("http://slow-api.com"),
timeout=5.0
)
return result
except asyncio.TimeoutError:
print("Task timed out!")
return None
async def multiple_tasks():
"""Manage multiple tasks."""
# Create tasks
task1 = asyncio.create_task(fetch_data("http://api1.com"))
task2 = asyncio.create_task(fetch_data("http://api2.com"))
task3 = asyncio.create_task(fetch_data("http://api3.com"))
# Wait for all tasks
done, pending = await asyncio.wait(
[task1, task2, task3],
return_when=asyncio.FIRST_COMPLETED
)
print(f"Completed: {len(done)}, Pending: {len(pending)}")
# Wait for remaining
remaining = await asyncio.gather(*pending)
return done, remaining
async def task_with_callbacks():
"""Add callbacks to tasks."""
def callback(task):
try:
result = task.result()
print(f"Task completed: {result['url']}")
except Exception as e:
print(f"Task failed: {e}")
task = asyncio.create_task(fetch_data("http://api.com"))
task.add_done_callback(callback)
await task
Error Handling
async def safe_gather():
"""Handle errors in gather."""
async def risky_task(name: str, should_fail: bool = False):
await asyncio.sleep(0.5)
if should_fail:
raise ValueError(f"Task {name} failed!")
return f"Task {name} succeeded"
# Option 1: return_exceptions=True
results = await asyncio.gather(
risky_task("task1"),
risky_task("task2", should_fail=True),
risky_task("task3"),
return_exceptions=True
)
for i, result in enumerate(results):
if isinstance(result, Exception):
print(f"Task {i+1} failed: {result}")
else:
print(f"Task {i+1} succeeded: {result}")
# Option 2: asyncio.wait
tasks = [
asyncio.create_task(risky_task("task1")),
asyncio.create_task(risky_task("task2", should_fail=True)),
asyncio.create_task(risky_task("task3"))
]
done, pending = await asyncio.wait(tasks)
for task in done:
if task.exception():
print(f"Exception: {task.exception()}")
else:
print(f"Result: {task.result()}")
JavaScript Async/Await
Modern Async Patterns
// Basic async/await
async function fetchData(url) {
try {
const response = await fetch(url);
const data = await response.json();
return data;
} catch (error) {
console.error('Fetch failed:', error);
throw error;
}
}
// Concurrent requests
async function fetchAll(urls) {
// Promise.all for concurrent execution
const promises = urls.map(url => fetch(url).then(r => r.json()));
const results = await Promise.all(promises);
return results;
}
// Promise.allSettled for error handling
async function fetchWithSettled(urls) {
const results = await Promise.allSettled(
urls.map(url => fetch(url).then(r => r.json()))
);
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
console.log(`URL ${index}:`, result.value);
} else {
console.log(`URL ${index} failed:`, result.reason);
}
});
}
// Promise.race - first to complete
async function fetchWithTimeout(url, timeout = 5000) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
try {
const response = await fetch(url, { signal: controller.signal });
return await response.json();
} finally {
clearTimeout(timeoutId);
}
}
Async Iterators
// Async iterator for streaming data
async function* fetchPages(baseUrl) {
let page = 1;
let hasMore = true;
while (hasMore) {
const response = await fetch(`${baseUrl}?page=${page}`);
const data = await response.json();
if (data.length === 0) {
hasMore = false;
} else {
yield data;
page++;
}
}
}
// Using async iterator
async function processAllPages() {
for await (const page of fetchPages('https://api.example.com/items')) {
console.log(`Processing ${page.length} items`);
// Process each page
}
}
// Async generator for real-time data
async function* streamEvents() {
const eventSource = new EventSource('/api/events');
try {
while (true) {
const event = await new Promise((resolve, reject) => {
eventSource.onmessage = (e) => resolve(e.data);
eventSource.onerror = (e) => reject(e);
});
yield JSON.parse(event);
}
} finally {
eventSource.close();
}
}
Advanced Patterns
// Rate limiting with async
class RateLimiter {
constructor(maxRequests, timeWindow) {
this.maxRequests = maxRequests;
this.timeWindow = timeWindow;
this.requests = [];
}
async acquire() {
const now = Date.now();
this.requests = this.requests.filter(t => now - t < this.timeWindow);
if (this.requests.length >= this.maxRequests) {
const oldest = this.requests[0];
const waitTime = this.timeWindow - (now - oldest);
await new Promise(r => setTimeout(r, waitTime));
return this.acquire();
}
this.requests.push(now);
}
}
// Using rate limiter
const limiter = new RateLimiter(10, 1000); // 10 requests per second
async function fetchWithRateLimit(url) {
await limiter.acquire();
return fetch(url);
}
// Retry with exponential backoff
async function fetchWithRetry(url, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
try {
return await fetch(url);
} catch (error) {
if (i === maxRetries - 1) throw error;
const delay = Math.pow(2, i) * 1000;
await new Promise(r => setTimeout(r, delay));
}
}
}
Threading vs Async
When to Use Threads
import threading
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor
def cpu_intensive_task(n):
"""CPU-bound computation."""
result = sum(i * i for i in range(n))
return result
# Use ProcessPoolExecutor for CPU-bound tasks
def parallel_cpu_work():
with ProcessPoolExecutor(max_workers=4) as executor:
futures = [executor.submit(cpu_intensive_task, 10**7) for _ in range(4)]
results = [f.result() for f in futures]
return results
# Use ThreadPoolExecutor for I/O-bound tasks
def io_bound_task(url):
import requests
return requests.get(url).status_code
def parallel_io_work():
urls = ["http://example.com" for _ in range(10)]
with ThreadPoolExecutor(max_workers=10) as executor:
results = list(executor.map(io_bound_task, urls))
return results
Async with Threading
import asyncio
from concurrent.futures import ThreadPoolExecutor
def blocking_io_operation():
"""Blocking operation that needs to run in thread."""
import time
time.sleep(1) # This blocks!
return "Done"
async def main():
loop = asyncio.get_event_loop()
# Run blocking operation in thread pool
result = await loop.run_in_executor(
ThreadPoolExecutor(),
blocking_io_operation
)
print(f"Result: {result}")
# For CPU-bound work in async
def cpu_work():
return sum(i * i for i in range(10**7))
async def main_with_cpu():
loop = asyncio.get_event_loop()
# Run CPU work in process pool
result = await loop.run_in_executor(
ProcessPoolExecutor(),
cpu_work
)
print(f"CPU result: {result}")
Best Practices
| Practice | Implementation |
|---|---|
| Use async consistently | Don’t mix sync/async |
| Avoid blocking calls | Use async libraries |
| Handle errors properly | Use try/except in await |
| Limit concurrency | Use semaphores |
| Set timeouts | Prevent hangs |
| Use gather for parallelism | Concurrent execution |
Common Pitfalls
# โ BAD: Blocking the event loop
async def bad_example():
import time
time.sleep(1) # Blocks entire event loop!
return "Done"
# โ
GOOD: Using async sleep
async def good_example():
await asyncio.sleep(1) # Non-blocking
return "Done"
# โ BAD: Not awaiting
async def bad_await():
fetch_data("http://example.com") # Forgot await!
# โ
GOOD: Proper await
async def good_await():
await fetch_data("http://example.com")
# โ BAD: Sequential when concurrent possible
async def bad_sequential():
r1 = await fetch("url1")
r2 = await fetch("url2")
r3 = await fetch("url3")
# โ
GOOD: Concurrent with gather
async def good_concurrent():
r1, r2, r3 = await asyncio.gather(
fetch("url1"),
fetch("url2"),
fetch("url3")
)
Performance Comparison
import asyncio
import time
async def measure_performance():
urls = [f"http://api.example.com/item/{i}" for i in range(100)]
# Sequential
start = time.time()
for url in urls:
await fetch_data(url, 0.01)
sequential_time = time.time() - start
# Concurrent
start = time.time()
await asyncio.gather(*[fetch_data(url, 0.01) for url in urls])
concurrent_time = time.time() - start
print(f"Sequential: {sequential_time:.2f}s")
print(f"Concurrent: {concurrent_time:.2f}s")
print(f"Speedup: {sequential_time / concurrent_time:.1f}x")
Conclusion
Asynchronous programming is essential for building high-performance applications, especially I/O-bound systems. By understanding the difference between concurrency and parallelism and applying the patterns in this guide, you can dramatically improve your application’s performance.
Key takeaways:
- Concurrency vs parallelism - Concurrency handles multiple tasks, parallelism runs them simultaneously
- Use async for I/O - Network requests, file operations, databases
- Use processes for CPU - Computation-intensive tasks
- Avoid blocking - Never use sync operations in async code
- Use gather - Run independent async tasks concurrently
- Handle errors - Use try/except and return_exceptions
By mastering these async patterns, you’ll build faster, more responsive applications.
Comments