3. Scheduler & Memory Model
How the runtime schedules goroutines, grows stacks, and what the memory model guarantees.
Q1 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
GOMAXPROCSPs.
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.
Q2 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.
Q3 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
Unlockon async.Mutexhappens 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.
Q4 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.
Q5 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.
Q6 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.
Q7 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.