3. Scheduler & Memory Model

How the runtime schedules goroutines, grows stacks, and what the memory model guarantees.

Question: Can you briefly explain Go's G-M-P scheduler model?

Answer: The Go scheduler uses a G-M-P model:

  • G: Goroutine, a lightweight thread.

  • M: Machine thread, a standard OS thread.

  • P: Processor, a logical processor that represents a resource for executing Go code. There are GOMAXPROCS Ps.

Explanation: The scheduler's job is to distribute runnable Goroutines (Gs) onto a pool of OS threads (Ms), with each M paired with a logical Processor (P). This model allows Go to multiplex a large number of goroutines onto a small number of OS threads, making concurrency cheap. It also enables work-stealing, where a P with no runnable Gs can steal them from another P, ensuring all threads are kept busy.

Question: How does the Go scheduler handle blocking system calls?

Answer: When a goroutine makes a blocking system call (like file I/O), the scheduler detaches the M executing it from its P and may spin up a new M to run other goroutines from that P's queue. This prevents one blocking goroutine from stopping all other goroutines that could be running on the same OS thread.

Explanation: This intelligent handling of blocking calls is a key reason Go's concurrency model is so efficient. It avoids wasting CPU time while waiting for I/O and keeps the logical processors utilized.

Question: What does Go's memory model guarantee regarding the "happens-before" relationship?

Answer: Go's memory model specifies the conditions under which a read of a variable in one goroutine is guaranteed to observe a write to that same variable in another. This "happens-before" relationship is established by explicit synchronization primitives.

Explanation: The primary synchronization events that establish happens-before are:

  • A send on a channel happens before the corresponding receive from that channel completes.

  • The closing of a channel happens before a receive that returns a zero value because the channel is closed.

  • An Unlock on a sync.Mutex happens before a subsequent Lock.

  • A call to sync.Once.Do(f) happens before the return from f.

Relying on timing or time.Sleep for synchronization is incorrect; always use explicit synchronization. When in doubt, use a channel or a mutex.

Question: What memory ordering do sync/atomic operations provide?

Answer: Atomic loads/stores/CAS establish happens-before for the accessed variable; do not mix atomic and non-atomic access to the same variable.

Explanation: Prefer mutexes for multi-variable invariants; atomics suit simple shared variables.

Question: How does preemption work and why does it matter?

Answer: Go's scheduler preempts long-running goroutines at safe points (function calls, loop back-edges, syscalls) to maintain fairness and responsiveness.

Explanation: Preemption prevents a CPU-bound goroutine from starving others. Since Go 1.14, asynchronous preemption improves latency by injecting preemption at more points.

Question: How do goroutine stacks work?

Answer: Goroutines start with small stacks (a few KB) that grow and shrink dynamically.

Explanation: Stack growth is handled by the runtime and is usually cheap. Avoid large stack allocations (e.g., huge arrays) which can trigger costly growth; allocate such data on the heap.

Question: What does the race detector do and when should you use it?

Answer: The race detector (go test -race, go run -race) detects data races at runtime by instrumenting memory accesses.

Explanation: It slows execution but is invaluable in CI and during development for concurrent code paths. Fix races rather than suppressing them; they indicate undefined behavior across goroutines.