// lesson: anatomy-of-a-goroutine

The Anatomy of a Goroutine

You have written go f() a thousand times. This course is about what happens next โ€” and the best way to understand it is to build the machinery yourself, in ordinary Go, one piece at a time.

Start with the object itself. A goroutine is not a thread. It is a small heap structure the runtime calls a g, defined in runtime/runtime2.go:

// Heavily abridged from runtime/runtime2.go.
type g struct {
	stack       stack   // [stack.lo, stack.hi) โ€” the goroutine's stack bounds
	stackguard0 uintptr // stack-growth (and preemption!) check value
	sched       gobuf   // saved SP, PC, etc. โ€” the "context" in context switch
	atomicstatus atomic.Uint32 // _Grunnable, _Grunning, _Gwaiting, ...
	goid        uint64
	waitreason  waitReason // why it's parked ("chan receive", "sleep", ...)
}

Three things make a g cheap where an OS thread is expensive:

  • Tiny, growable stacks. A goroutine starts with a ~2 KB stack (since Go 1.19 the runtime may pick a larger start size based on the historical average). Every function prologue compares the stack pointer against stackguard0; on overflow, morestack allocates a bigger stack and copies the old one over โ€” contiguous stacks. An OS thread reserves megabytes of virtual address space up front and can never shrink it.
  • Userspace context switches. Switching goroutines means saving a handful of registers into g.sched (a gobuf: SP, PC, and a pointer to the g) and loading another goroutine's. No syscall, no kernel scheduler, roughly the cost of a function call. The assembly routines are gogo and mcall.
  • M:N scheduling. Millions of Gs are multiplexed onto a few OS threads.

The runtime's scheduler is described by three letters you'll see constantly in runtime/proc.go:

  • G โ€” a goroutine: stack + saved registers + status.
  • M โ€” a machine: an actual OS thread that executes Gs.
  • P โ€” a processor: a scheduling context holding a local run queue of runnable Gs (plus allocation caches). There are exactly GOMAXPROCS Ps. An M must hold a P to run Go code; an M without a P is parked or sitting in a syscall.

go f() compiles to a call to runtime.newproc, which allocates (or recycles) a g, seeds its gobuf so it will "return into" f, marks it _Grunnable, and drops it on the current P's run queue. That's all โ€” the statement returns immediately, and f runs whenever a P picks it up. This is also why main returning kills the program without waiting for anyone: nothing counts outstanding goroutines. The runtime deliberately has no "join"; if you want one, you build it from synchronization primitives โ€” which is exactly your first challenge.

โ€บ A WaitGroup from Scratch

20 pts

Build the "join" primitive the runtime doesn't give you. Implement two package-level functions:

func Spawn(f func())  // start f concurrently; return immediately
func WaitAll()        // block until every spawned function has returned

Semantics your implementation must satisfy:

  • WaitAll returns immediately when nothing is in flight (including before the first Spawn ever happens).
  • WaitAll blocks until every spawned function has returned โ€” including functions spawned by spawned functions. As long as a running spawned function calls Spawn before it returns, the count never touches zero, so transitive work is covered automatically.
  • The package is reusable: after WaitAll returns you can Spawn again and WaitAll again.

sync.WaitGroup is banned โ€” using it would be building a WaitGroup out of a WaitGroup. (The grader can't truly detect it; this is between you and the duck.) The intended shape is an atomic counter plus a channel used as a one-shot park/unpark signal: Spawn increments before starting the goroutine, a deferred decrement detects the drop to zero and wakes waiters. A sync.Mutex guarding the counter alongside a "currently idle" channel is also a fine design. Think hard about the classic bug: incrementing inside the new goroutine instead of before it starts โ€” a WaitAll racing that increment sees zero and returns early.

Log in to submit a solution and earn points.