asyncio: Asynchronous I/O in Python
Python's asyncio library provides a foundation for writing concurrent code using the async/await syntax. Unlike threading, asyncio uses a single thread and a cooperative event loop โ making it ideal for I/O-bound workloads with many concurrent operations.
Why asyncio?
Threads have overhead: each thread consumes memory (~8MB stack), and context switching between them is expensive. With asyncio you can handle thousands of concurrent I/O operations in a single thread. The trade-off: you must explicitly yield control at every await point.
async def and await
Define a coroutine using async def. Use await to pause execution until an awaitable completes, yielding control back to the event loop in the meantime:
asyncio.sleep() is the async equivalent of time.sleep(). It suspends the coroutine without blocking the thread, allowing the event loop to run other coroutines.
asyncio.run() โ The Entry Point
asyncio.run() creates an event loop, runs a coroutine to completion, then closes the loop. Use it as your program's entry point:
Coroutines vs Threads
| Aspect | Threads | asyncio Coroutines |
|---|---|---|
| Concurrency model | Preemptive (OS switches) | Cooperative (you yield with await) |
| Memory per unit | ~8MB | ~1KB |
| Max concurrent | ~1,000s | ~100,000s |
| Blocking code | OK (other threads run) | Breaks everything (blocks event loop) |
| GIL | Limited CPU parallelism | N/A โ single-threaded |
| Best for | I/O with blocking libs | I/O with async libs |
Critical rule: Never call blocking functions (like time.sleep(), requests.get(), file I/O with normal open()) inside async code. They block the event loop, preventing all other coroutines from running. Use async equivalents: asyncio.sleep(), aiohttp, aiofiles.
asyncio.gather() โ Run Coroutines Concurrently
gather() runs multiple coroutines concurrently and returns all results when they all complete:
asyncio.create_task() โ Background Tasks
create_task() schedules a coroutine to run concurrently without awaiting it immediately. This lets you fire off work and do other things:
The difference from gather(): tasks created with create_task() run independently even if you don't immediately await them. gather() is cleaner when you want all results together.
The Event Loop
The event loop is the heart of asyncio. It manages a queue of coroutines and callbacks, running them one at a time, switching between them at every await:
await asyncio.sleep(0) yields control without actually sleeping โ useful to let other coroutines make progress in a tight loop.
asyncio.Queue โ Producer/Consumer Pattern
asyncio.Queue is a thread-safe (event-loop-safe) queue for passing data between coroutines:
When to Use asyncio
Use asyncio when:
- Making many concurrent HTTP requests (with
aiohttp) - Building network servers/clients
- Interacting with async databases (asyncpg, motor)
- High-concurrency I/O where threads would be too heavy
Stick with threads when:
- Using libraries that don't have async equivalents
- Running simple scripts with moderate concurrency
Use multiprocessing when:
- CPU-bound parallel computation
asyncio.wait_for() โ Timeouts
wait_for() cancels the coroutine automatically when the timeout expires.
Knowledge Check
What happens when you call an `async def` function without `await`?
What is the correct way to run multiple coroutines concurrently in asyncio?
Why should you never call `time.sleep()` inside an async function?