✎ Edit content·DAY 022 · POST 3 OF 5 · How It Works

Async/Await in Python

Python · 12 slides
DAY 022 · POST 3 OF 5
(REMINDER)
DAY 022
How Async Works
@saurav_dnj_24github.com/SauravDnj · linkedin.com/in/sauravdnj
1 / 12

Theme

Palette

Download

4K — sharpest, slowest
🎬 Video options
Preparing preview…
Live preview · loops the “none” effect
All rendering runs in your browser. No server, no cost, no upload. MP4/WebM = full motion + effects · GIF = lightweight loop · PNG/PDF = static for the Instagram & LinkedIn carousel.

Caption (tap to copy)

📖 Deep dive (full written explanation)

The slides stay clean and scannable. Here's the in-depth explanation behind each one — great for the blog version, show notes, or studying the topic properly.
Slide 1 · How Async Works

This is the engine-room post. The previous posts told you what async is and why it matters; this one shows the machine so the rules become predictable. The central image to hold is a coroutine as a state machine that the event loop repeatedly drives forward, one await at a time.

Once you see that switches happen only at await points and that the loop is a single driver picking among ready tasks, the surprising behaviors — why one blocking call freezes everything, why you must await — stop being mysteries and become consequences.

Slide 2 · A coroutine is a state machine

Under the hood, 'async def' compiles to something closer to a generator than a normal function. Calling it does not execute the body; it constructs a coroutine object that holds a frozen execution frame — the instruction pointer and all local variables. That object knows how to advance to its next suspension point when driven.

Each time the loop resumes the coroutine, it runs forward until the next await, then saves its position and locals and yields control back. This is genuine suspend-and-resume: the function is paused exactly where it left off, with its state intact, ready to continue as if it had never stopped. That capability is the bedrock of the whole model.

Slide 3 · await yields, the loop catches

await is the explicit handoff. When a coroutine awaits an awaitable, it suspends and propagates a value up to the loop describing what it is waiting for — a Future that will be resolved when the underlying I/O completes. The coroutine is effectively saying 'wake me when this Future is done.'

The loop catches that, registers the coroutine as waiting on that Future, and is now free to run other ready tasks. When the I/O the Future represents completes, the loop marks the Future done and reschedules the coroutine, which resumes on the line right after the await with the result in hand. Nothing was blocked; the thread stayed busy the entire time.

Slide 4 · Run, park, resume

The flow diagram traces a single suspension cycle: the loop runs the coroutine until an await, the await suspends it and yields to the loop, the loop runs other ready tasks while this one's I/O is pending, the OS eventually signals the I/O is ready, and the loop resumes the coroutine right after its await with the result.

The critical property to absorb is that every transition out of a coroutine happens at an await and nowhere else. There is no preemption, no timer interrupt yanking control mid-statement. The coroutine runs uninterrupted between awaits, which makes async code easier to reason about — but also means a coroutine with no awaits never gives anyone else a turn.

Slide 5 · Inline await vs concurrent tasks

This snippet contrasts the two ways to combine awaitables. Awaiting work(1) and then work(2) on separate lines is sequential: the second await does not even begin until the first fully completes, so the total is the sum, about three seconds. Inline await means 'do this and wait for it before moving on.'

asyncio.gather(work(1), work(2)) is different: it schedules both as tasks that run concurrently, so their sleeps overlap and the total is the slowest one, about two seconds. The lesson that recurs throughout async: await in sequence when you genuinely need ordering, but gather (or tasks) when the operations are independent and you want overlap.

Slide 6 · Task = scheduled coroutine

A Task is the bridge from 'a coroutine exists' to 'a coroutine is running concurrently.' asyncio.create_task wraps a coroutine and registers it with the loop, which begins driving it at the next opportunity, independently of the code that created it. That is what makes things actually overlap rather than run one after another.

Plain await runs a coroutine inline and waits for its result — useful but sequential. gather is the convenience that turns a batch of coroutines into tasks and waits for them all. The mental shorthand: bare coroutine is a recipe, Task is the recipe handed to a cook who starts immediately, await is you standing there until the dish is done.

Slide 7 · The loop's ready queue

The stack diagram exposes the loop's internal structure. At the top is the event loop itself, the single driver. It maintains a ready queue of tasks that can run right now, and a separate waiting set of tasks parked on I/O or timers. Underneath sits the OS selector — epoll, kqueue, or similar — that tells the loop which sockets have become readable or writable.

The loop's cycle is to drain the ready queue, run each task until it awaits and moves to the waiting set, then ask the selector which waits have completed and move those tasks back to ready. This layered design is why async is efficient: the loop is never busy-spinning, it sleeps in the selector until the OS wakes it with real work.

Slide 8 · Watch the interleave

This example makes the interleaving visible. Two tickers each print three lines, yielding with 'await asyncio.sleep(0)' after each. sleep(0) is a deliberate, minimal yield — it does no real waiting, it just gives the loop a chance to switch. gather runs both tickers concurrently.

The output alternates: A 0, B 0, A 1, B 1, and so on. That interleaving is the loop in action — each yield hands control back, the loop picks the other ready task, and they take turns. Remove the await and one ticker would run to completion before the other started, because without a yield point the loop never gets the chance to switch. This is the clearest demonstration that switches happen only at await.

Slide 9 · How it watches I/O

This explains why async is efficient rather than wasteful. A naive scheduler might constantly poll every connection asking 'are you ready yet?', burning CPU on no. The event loop does the opposite: it registers all pending sockets with an OS facility — epoll on Linux, kqueue on macOS — and then blocks, sleeping, until the OS itself reports which sockets are now readable or writable.

The loop wakes only when there is real work, and it wakes exactly the tasks whose I/O is ready. This OS-level readiness notification is the reason ten thousand idle connections cost almost nothing: they are entries the kernel watches, not threads the loop polls. The expensive waiting is delegated to the operating system, which is built to do it.

Slide 10 · One blocking call jams the loop

This is the single most important operational rule, and it falls straight out of the machine. Because the loop only switches at await, any code that runs without awaiting holds the thread hostage. A time.sleep, a synchronous requests.get, or a tight CPU loop never yields, so the loop cannot advance any other task — they all starve until the blocking call returns.

The symptom is brutal and confusing: your async server appears hung, latency spikes across every unrelated request, and nothing looks wrong in the blocked task itself. The fixes are to await an async equivalent (asyncio.sleep, an async client) or, for unavoidable blocking work, to offload it to a thread pool via loop.run_in_executor so the loop stays free.

Slide 11 · The model in one breath

These five lines compress the entire engine into something portable. A coroutine is a freezable state machine. await is an explicit yield point and the only place control leaves a coroutine. The loop picks ready tasks, parks them on await, and resumes them when ready. A Task is a coroutine scheduled to overlap with others. And crucially, switches happen only at await — coroutines are never preempted mid-run.

That last point is double-edged and worth holding onto. It makes async code easier to reason about than threaded code, because you know exactly where interleaving can occur. But it also means cooperation is mandatory: a coroutine that never yields breaks the whole loop.

Slide 12 · Save this. Follow for Day 23.

That completes the mechanics: coroutines as resumable state machines, await as the explicit handoff to a single-threaded loop, Tasks as the unit of overlap, and an OS selector doing the heavy waiting — all adding up to why blocking calls are fatal and why you must await.

Day 23 in this slot gets hands-on: a complete, runnable example that fetches many URLs concurrently with asyncio and aiohttp, turning all this theory into the pattern you will reach for most often in practice.

🎨 AI image prompt (matches this theme + palette)

Paste into Midjourney, DALL·E, Ideogram, etc. to generate an on-brand image, then upload it on the Edit content page. The prompt updates automatically with the selected theme + palette.