โšกasync/await, event loop, coroutines, tasksLESSON

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

AspectThreadsasyncio Coroutines
Concurrency modelPreemptive (OS switches)Cooperative (you yield with await)
Memory per unit~8MB~1KB
Max concurrent~1,000s~100,000s
Blocking codeOK (other threads run)Breaks everything (blocks event loop)
GILLimited CPU parallelismN/A โ€” single-threaded
Best forI/O with blocking libsI/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?