Advanced Concurrency & Asynchrony

stackful versus stackless coroutines

Two ways to bookmark your place when you step away. One: keep your whole desk exactly as it is — papers spread, pen mid-sentence — and just walk away; when you return everything is untouched and you continue. Two: jot a tiny note 'I was on step 4, with x=7', clear the desk for someone else, and rebuild from the note when you come back. Coroutines split the same way. A stackful coroutine keeps its own entire call stack alive while suspended; a stackless one saves only the minimal state needed to resume, in a compact object.

A stackful coroutine — a fiber, a green thread, a Go goroutine — owns a real (if small) stack, separate from any OS thread's. Suspending it means saving the CPU registers and switching the stack pointer to another fiber's stack; the suspended fiber's stack sits in memory frozen. Because it has a full stack, it can suspend from anywhere, even deep inside a called function, and await is invisible — code looks plain. The cost is a stack per coroutine (Go starts goroutines at ~2 KiB and grows them) and a context-switch on every suspend. A stackless coroutine has no stack of its own. The compiler transforms an async function into a state machine: a struct holding the function's live local variables plus an integer recording which await it is paused at. Suspending is just returning from a poll() call; resuming is calling poll() again, which jumps to the saved state. This is Rust async and C++20 coroutines. Suspended state is tiny (just the live locals), but you can only suspend at explicit await points in the async function itself, not from inside an ordinary called function.

The trade-off is the heart of modern async design. Stackful is ergonomic and uniform (any function can yield, no function colouring) but pays per-coroutine memory and switch cost. Stackless is extremely lightweight — a suspended Rust task can be a few dozen bytes — and needs no separate stack, but forces the 'async colours functions' rule (an async function can only be awaited from async code) and the whole call must be built from awaitable pieces. There is no universally 'better'; Go bet on stackful for simplicity, Rust on stackless for zero-cost density. Both are cooperatively scheduled and both suspend and resume; they differ in what they save and where they can pause.

Stackful (Go): go work(); // work() can block-and-yield anywhere, on its own ~KiB stack. Stackless (Rust): the compiler rewrites 'async fn f' into a state machine struct { state: u8, /* live locals */ } with a poll() method.

Stackful parks a whole stack; stackless saves only the live locals plus a 'which await' tag in a compact state machine.

Common confusion: goroutines are stackful coroutines multiplexed onto OS threads, not OS threads. And stackless 'colours' functions — async can only be awaited from async — which is a design constraint, not a bug; stackful avoids that at the cost of per-coroutine stacks.

Also called
fibers vs state machinesgreen threads vs async/await纖程與狀態機