skip to content

questions

5

What is a coroutine, and what actually happens when one suspends, compared with an operating-system thread that blocks waiting for I/O?

level: juniorimportance: must knowfreq 58%

answer

  1. pause and resume, not stop and restart
  2. suspend frees the carrier thread; block does not
  3. state in a heap record, not a reserved stack
  4. cooperative: control moves only at suspension points
  5. concurrency unbounded, parallelism = carrier threads

basics

~20 s

A coroutine is a function that can pause partway through and resume later. Suspending saves its state, hands control back to a scheduler, and frees the underlying thread for other work. A blocked thread keeps its whole stack and OS slot while doing nothing.

solid answer

~50 s

A **coroutine** is a computation that can pause at defined points and later resume exactly where it stopped, carrying its local variables with it. When a coroutine **suspends**, the runtime captures its resume point and live locals, registers interest in whatever it is waiting for, and returns control to the scheduler. The carrier thread is then free to run other coroutines. When the awaited event completes, the scheduler resumes the coroutine, possibly on a different carrier thread. When an OS thread **blocks**, the kernel deschedules it, but the thread still exists: its stack (hundreds of kilobytes to megabytes of reserved address space), its kernel task structure, and its scheduler entry stay reserved. Nothing else can use it. The difference is who pays for the wait. Blocking costs one whole thread per outstanding operation; suspension costs one small heap object. That is why a process can hold hundreds of thousands of suspended coroutines but not hundreds of thousands of blocked threads.

go deeper

for a junior

Define a coroutine as a function that can pause and resume, and state the core contrast: suspending releases the thread, blocking holds it hostage.

for a middle

Add the mechanics — the continuation record, the carrier-thread pool, and the cost comparison of a heap object versus a reserved stack plus kernel context switch.

for a senior

Emphasise the cooperative-scheduling consequence: no suspension point means no yield, so CPU-bound work starves the pool. Note that resumption can move threads, breaking thread-affine assumptions.

for a principal

Frame it as a resource-model choice: coroutines decouple in-flight concurrency from thread count so capacity scales with outstanding requests instead of cores, at the cost of needing every I/O path in the stack to be non-blocking.

## The problem coroutines solve A server handling many slow operations must remember, for each one in flight, "where was I, and what do I do when the answer arrives?" The traditional place to store that memory is a thread: the call stack *is* the record of where you were. It reads beautifully — you write straight-line code — but it is expensive. Each thread reserves a contiguous stack sized for the deepest call it might ever make, plus kernel bookkeeping. Ten thousand concurrent waits means ten thousand stacks sitting idle. A coroutine keeps the same straight-line readability but stores "where was I" in a much smaller, heap-allocated record instead of a reserved stack. ## Definition A coroutine is a routine with more than one entry point: it can *yield* control back to its caller or scheduler and later be re-entered at the point it left off, with its locals intact. Ordinary functions have exactly one entry (the top) and give up their state when they return. Coroutines generalise that. ## Suspend versus block - **Block**: the calling thread makes a request the kernel cannot satisfy immediately. The kernel marks the thread not-runnable and switches to another thread. The blocked thread's stack, registers, and task structure remain allocated until the wait ends. Cost: one thread per concurrent wait, plus a kernel context switch (typically a few microseconds) on each transition. - **Suspend**: the coroutine reaches a suspension point, the runtime stores its continuation (resume point plus live locals) into a small object, arranges to be notified when the awaited result is ready, and *returns* to the scheduler. Cost: one small allocation and a normal function return; commonly tens to hundreds of nanoseconds. No kernel involvement at all in the common case. Crucially, the thread that was running the coroutine is not waiting for anything. It goes back to the scheduler and picks up the next ready coroutine. ## Cooperative, not preemptive OS threads are **preemptive**: a timer interrupt can take the CPU away from a thread at essentially any machine instruction. Coroutines are **cooperative**: control only changes hands at a suspension point the code chose. That has two consequences. The good one: interleaving is bounded and visible. Between two suspension points, a coroutine runs without another coroutine on the same loop interleaving with it, which simplifies reasoning about invariants. The bad one: a coroutine that never suspends never yields. A long CPU-bound computation with no suspension point starves every other coroutine on that carrier thread. The scheduler has no way to intervene — that is what "cooperative" means. ## Carrier threads and multiplexing Coroutines do not execute themselves; they run on some pool of real threads, often called carrier or worker threads. A common shape is N carrier threads for N cores, multiplexing many thousands of coroutines. Concurrency (many things in progress) is unbounded; parallelism (many things executing at the same instant) is capped by the carrier pool. A coroutine that is suspended occupies no thread at all. Because a coroutine may resume on a different carrier thread than it suspended on, thread-affine state — thread-local variables, thread-owned locks, thread identity used as an ownership token — becomes unsafe in a way it is not with plain threads. ## Why it matters in practice The payoff is memory and switching cost per concurrent operation. A workload that is 99% waiting (network calls, database round trips) needs concurrency proportional to the number of outstanding requests, not to CPU count. Coroutines make that number cheap, so a single process can hold far more in-flight work than its thread budget would allow, while still being written as ordinary sequential-looking code rather than a chain of callbacks.

  • If a coroutine runs a long computation and never reaches a suspension point, what does the scheduler do about it?
    Nothing — that is the defining limitation of cooperative scheduling. The carrier thread is occupied until the coroutine either suspends or finishes, so every other coroutine assigned to that thread is delayed. The fix is in the code: insert explicit yield points, chunk the work, or move it to a pool dedicated to CPU-bound tasks.
  • Do coroutines make a program parallel?
    No. Coroutines give you cheap concurrency — many operations in progress. Parallelism comes from the number of carrier threads and cores underneath. A million coroutines on one carrier thread execute strictly one at a time; the win is that waiting ones consume almost no resources.

A blocked thread is a checkout lane closed because the cashier is on the phone with a supplier — the lane is unusable. A suspended coroutine is the cashier setting your basket aside, serving the next customer, and picking your basket back up when the supplier calls back.

saying these in an interview costs you the question

  • Saying coroutines are scheduled by the OS kernel — they are scheduled in user space by the runtime
  • Claiming suspension is the same as sleeping, or that it blocks the thread briefly
  • Claiming coroutines make code faster or parallel by themselves
  • Assuming a coroutine resumes on the same thread it suspended on, so thread-local state is safe
  • Describing suspension as preemptive — the runtime cannot interrupt a coroutine that will not yield

context

open as a page

If tasks in a runtime switch only at explicit suspension points and share one carrier thread, can you still get race conditions? Explain what is and is not guaranteed.

level: seniorimportance: must knowfreq 42%

basics

~20 s

Yes. Single-threaded cooperative scheduling removes low-level data races — no two tasks touch memory at the same instant — but not logical races. Any suspension point is a place where arbitrary other tasks ran, so check-then-act sequences spanning an await can still be violated.

open as a page

Compare stackful and stackless coroutines: how does each remember where it was, and what can one do that the other cannot?

level: middleimportance: should knowfreq 44%

basics

~20 s

A stackful coroutine owns a real stack, so it can suspend from any depth, including inside a function that knows nothing about coroutines. A stackless one is compiled into a state machine holding only its own locals, so it can suspend only at explicit points in itself, but costs far less memory.

open as a page

How does a compiler turn a sequential-looking function containing await points into something that can pause and resume? Explain the continuation-passing / state-machine transform.

level: seniorimportance: should knowfreq 34%

basics

~20 s

It splits the body at each await into separate blocks and rewrites it as a state machine: an object holds a state number plus every local that outlives an await. Resuming calls a resume function that switches on the state and jumps to the right block. The continuation is the callback you never wrote.

open as a page

In some runtimes, calling an asynchronous function forces the caller to be asynchronous too, splitting an API into two parallel worlds. What causes that split, what problems does it create, and which designs avoid it?

level: principalimportance: should knowfreq 30%

basics

~20 s

It comes from the stackless transform: only rewritten frames can pause, so the ability to pause must propagate to every caller. That duplicates APIs, blocks incremental adoption, and tempts unsafe sync-over-async bridging. Stackful coroutines, user-mode threads, and effect systems remove the split.

open as a page