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 async.Mutex
happens before a subsequentLock
.A call to
sync.Once.Do(f)
happens before the return fromf
.
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.