Async programming
Mid / SeniorThe event loop, coroutines, and where asyncio helps or hurts.
5 questions · answers hidden
01What actually happens when you `await` a coroutine?
#q-what-actually-happens-when-you-await-a-coroutineawaitsuspends the current coroutine and yields control back to the event loop, handing it an awaitable. The event loop registers a callback to resume the coroutine when that awaitable completes (for example, when a socket becomes readable) and is then free to run other ready tasks.Key point:
awaitdoes not create concurrency by itself.await foo()runsfooto completion before the next line, exactly like a synchronous call from the caller’s point of view. Concurrency comes from having multiple tasks scheduled on the loop — created withasyncio.create_task,asyncio.gather, or aTaskGroup— so that while one is suspended on I/O the loop can advance another.# Sequential: ~2s total await fetch(a) await fetch(b) # Concurrent: ~1s total await asyncio.gather(fetch(a), fetch(b))02Why can a single blocking call freeze an entire asyncio application?
#q-why-can-a-single-blocking-call-freeze-an-entire-asyncio-applThere is one event loop thread. If a coroutine runs CPU-bound code or calls a blocking synchronous API (a non-async DB driver,
time.sleep,requests.get), it never yields to the loop, so every other task is starved until it returns — including timers, health checks, and cancellation.Fixes:
- Use async-native libraries for I/O (
asyncpg,httpx,aiofiles). - Push blocking or CPU-bound work off the loop with
await asyncio.to_thread(func, ...)(threads, good for blocking I/O) or aProcessPoolExecutorvialoop.run_in_executor(processes, good for CPU work). - Keep individual coroutine steps short so the loop stays responsive.
- Use async-native libraries for I/O (
03When is asyncio the wrong tool?
#q-when-is-asyncio-the-wrong-tool- CPU-bound workloads. The GIL and the single loop thread mean you get no parallelism;
multiprocessingor a native extension is the answer. - Simple, low-concurrency scripts. The overhead in complexity (coloured functions, careful cancellation, different libraries) buys nothing if you are making ten HTTP calls in a cron job — a thread pool is simpler.
- Codebases dominated by mature sync libraries. Mixing paradigms tends to leak blocking calls onto the loop. Threads may integrate more cleanly.
asyncio shines when you have many concurrent, mostly-idle I/O connections (sockets, websockets, upstream services) and want to hold them cheaply on one thread.
- CPU-bound workloads. The GIL and the single loop thread mean you get no parallelism;
04How does cancellation work, and what's the common bug?
#q-how-does-cancellation-work-and-whats-the-common-bugCancelling a task raises
asyncio.CancelledErrorinside it at the next suspension point. Well-behaved code lets it propagate sofinallyblocks and context managers can clean up.The classic bug is swallowing it:
try: await something() except Exception: # CancelledError is a BaseException in 3.8+, but people still catch broadly log.exception("oops")If you catch
CancelledError(directly, or via an over-broad handler on older code paths) and do not re-raise, the task ignores cancellation — timeouts and shutdown hang. If you must run cleanup on cancellation, catch it, do the work, andraise.05What problem do `asyncio.TaskGroup` and structured concurrency solve?
#q-what-problem-do-asynciotaskgroup-and-structured-concurrency-Before
TaskGroup(3.11),create_taskproduced “background” tasks with no owner. If one failed, the exception surfaced late (or only as a “task exception was never retrieved” warning), and it was easy to leak tasks that outlived the scope that started them.TaskGroupties task lifetimes to a lexical scope:async with asyncio.TaskGroup() as tg: tg.create_task(worker(1)) tg.create_task(worker(2)) # all tasks are done hereIf any task raises, the group cancels the siblings and propagates the error(s) as an
ExceptionGrouponce the block exits. Nothing escapes theasync with.