🚀AdvancedConcurrency

Asyncio Async/Await Deep Dive

Event loops, coroutines, Tasks, gather, timeouts, and building real async pipelines.

Overview

`asyncio` is Python's built-in framework for writing single-threaded concurrent code using coroutines. Instead of blocking when waiting on I/O, a coroutine suspends itself (`await`) and yields control back to the event loop, which runs another coroutine. The result: thousands of concurrent I/O operations in one thread, with no GIL issues and less overhead than threads. **Coroutine**: an `async def` function. Calling it returns a coroutine object it doesn't run until awaited. **Task**: a coroutine wrapped by `asyncio.create_task()` that is scheduled to run concurrently. **Event Loop**: the scheduler that drives all coroutines. `asyncio.run(main())` is the standard entry point. Key patterns: `await asyncio.gather(*coros)` to run multiple coroutines concurrently and collect results; `asyncio.wait_for(coro, timeout)` for deadlines; `asyncio.Queue` for async producer-consumer; `async for` / `async with` for async iteration and context managers.

Key Points

  • •`async def` defines a coroutine; `await expr` suspends until expr resolves
  • •`asyncio.run(main())` the standard way to start an async program (Python 3.7+)
  • •`asyncio.create_task(coro)` schedules a coroutine as a Task to run concurrently
  • •`await asyncio.gather(t1, t2, t3)` run multiple awaitables concurrently; returns list of results
  • •`await asyncio.sleep(n)` async sleep that yields control (unlike time.sleep which blocks)
  • •`asyncio.wait_for(coro, timeout=5)` cancel the coroutine if it takes too long
  • •`asyncio.Queue` async-safe queue; use `await q.put()` and `await q.get()`
  • •`async with` and `async for` work with classes that implement `__aenter__/`__aexit__` or `__aiter__/`__anext__`

Examples

Coroutines running concurrently with gather

import asyncio

async def fetch(name, delay):
    print(f"start {name}")
    await asyncio.sleep(delay)   # non-blocking wait
    print(f"done  {name}")
    return f"result:{name}"

async def main():
    results = await asyncio.gather(
        fetch("A", 1),
        fetch("B", 2),
        fetch("C", 0.5),
    )
    print(results)

asyncio.run(main())
# Completes in ~2 s (longest task), not 3.5 s (sum)

Tasks for fire-and-forget with cancellation

import asyncio

async def worker(n):
    await asyncio.sleep(n)
    return n * n

async def main():
    t1 = asyncio.create_task(worker(1))
    t2 = asyncio.create_task(worker(2))
    t3 = asyncio.create_task(worker(3))

    # cancel t3 before it finishes
    t3.cancel()

    results = await asyncio.gather(t1, t2, return_exceptions=True)
    print(results)   # [1, 4]

asyncio.run(main())

Async producer-consumer with asyncio.Queue

import asyncio

async def producer(q):
    for i in range(6):
        await q.put(i)
        await asyncio.sleep(0.1)
    await q.put(None)

async def consumer(q):
    while True:
        item = await q.get()
        if item is None:
            break
        print(f"consumed {item}")
        q.task_done()

async def main():
    q = asyncio.Queue(maxsize=3)
    await asyncio.gather(producer(q), consumer(q))

asyncio.run(main())

Common Mistakes

  • Calling `time.sleep()` inside a coroutine blocks the entire event loop; always use `await asyncio.sleep()`
  • Not awaiting a coroutine `fetch("A")` creates a coroutine object but never runs it; you must `await` it
  • Using `asyncio.run()` inside an already-running event loop (e.g. Jupyter) use `await` directly or `asyncio.get_event_loop().run_until_complete()` instead

Note

asyncio shines for network I/O (HTTP clients with aiohttp, databases with asyncpg, etc.). For CPU work use `loop.run_in_executor(None, sync_fn)` to run synchronous code in a thread pool without blocking the event loop.

Exercises

Write Python code to solve each exercise. Run it in your browser and check your output.

Loading editor…

Completed: 0 / 5 exercises

Knowledge Check

Question 1 of 4

What does `await asyncio.sleep(1)` do differently from `time.sleep(1)`?