Skillversitas

asyncio Introduction

asyncio is Python's built-in framework for writing concurrent code with a single thread and an event loop. Instead of creating a thread per task, you write coroutines - functions that can pause and resume - and the event loop schedules thousands of them on one thread, switching at every await. The payoff is massive I/O concurrency with tiny memory overhead: a server handling 10,000 open connections does not need 10,000 threads.

The Event Loop

The event loop is a scheduler. It keeps a set of ready coroutines and I/O watchers; when a coroutine awaits I/O, the loop parks it and runs the next ready one; when the I/O completes, the loop resumes the parked coroutine. Everything - coroutines, I/O callbacks, timers - runs on the same thread, in order. There are no data races from thread interleaving, but a single slow synchronous call can stall every task in the loop.

First Example

import asyncio

async def say_after(delay, what):
    await asyncio.sleep(delay)
    print(what)

async def main():
    # three tasks overlap: total time ~3s, not 6s
    await asyncio.gather(
        say_after(1, "one"),
        say_after(2, "two"),
        say_after(3, "three"),
    )

asyncio.run(main())

asyncio.run(main()) creates a fresh event loop, runs main to completion, and then closes the loop - it is the standard entry point for an asyncio program.

Sync vs Async, Side by Side

import time
import asyncio
import requests

def sync_fetch(urls):
    for u in urls:
        requests.get(u)          # each call blocks the thread

async def async_fetch(urls):
    import aiohttp
    async with aiohttp.ClientSession() as session:
        await asyncio.gather(*(session.get(u) for u in urls))  # overlap

The sync version spends its life waiting; the async version waits without blocking, so all requests fly out almost at once.

The Two Rules

  • Inside an event loop, never call a blocking function (time.sleep, requests.get, synchronous DB drivers) - it freezes every other task. Use the async equivalents (asyncio.sleep, aiohttp, async drivers) or offload blocking work with asyncio.to_thread.
  • Await coroutines. Calling an async def without await creates a coroutine object that runs only if awaited; Python warns about coroutines that are never awaited.

Summary

asyncio gives you I/O concurrency on one thread: write coroutines, await every suspending call, and run everything through asyncio.run. It is the right model when hundreds or thousands of connections wait on the network, and the wrong model when the work is CPU-heavy.