Python asyncio: Coroutines, Event Loop & async/await

Why async beats threads for IO-bound work, coroutine basics, the event loop, await and gather, working with async databases and HTTP clients, and common pitfalls.

AdvancedUpdated 2026-10-2861K views

Why Asynchronous Programming

CPython has the GIL: at any moment only one thread executes Python bytecode, so threads do not speed up CPU-bound work. For IO-bound work (HTTP calls, database queries), async lets one thread switch to other tasks while waiting — thousands of connections with a single process.

ScenarioThreadsAsync
1000 idle HTTP connections1000 OS threads — heavy1 thread, trivial
CPU-heavy computationNo gain (GIL)No gain, event loop blocks
Many concurrent DB/HTTP waitsWorks, but thread switch costBest fit

Coroutines, async and await

import asyncio

async def fetch_user(uid: int) -> str:
    await asyncio.sleep(0.5)        # 模拟 IO 等待
    return f"user-{uid}"

# 调用协程返回的是协程对象,不会执行
# 必须由事件循环调度

async def main():
    # 串行:约 1 秒
    a = await fetch_user(1)
    b = await fetch_user(2)

    # 并发:约 0.5 秒
    a, b = await asyncio.gather(
        fetch_user(1), fetch_user(2)
    )

asyncio.run(main())   # 启动事件循环并运行
  • async def defines a coroutine function; calling it returns a coroutine object.
  • await suspends the coroutine until the awaited awaitable completes, yielding control back to the event loop.
  • asyncio.gather runs awaitables concurrently; asyncio.create_task schedules a task without waiting.

Real-world Async Clients

import asyncio
import aiohttp

async def fetch_titles(urls):
    async with aiohttp.ClientSession() as session:
        async def get(url):
            async with session.get(url) as resp:
                html = await resp.text()
                return html
        return await asyncio.gather(*[get(u) for u in urls])

asyncio.run(fetch_titles(["https://example.com" for _ in range(100)]))
Every library in the call chain must be async — requests is synchronous and will block the loop. Use aiohttp/httpx for HTTP, asyncpg/aiomysql or SQLAlchemy async for databases.

Common Pitfalls

  1. Forgetting await — the coroutine never runs and you get a 'coroutine was never awaited' warning.
  2. Mixing blocking calls (time.sleep, open(), requests) inside async code blocks the whole loop.
  3. Creating one asyncio.run per request instead of a long-lived loop.

Friends & Partners