Go

How does the runtime preempt goroutines, and what are GC safe points?

Question 368HardGo 1.22 to 1.25

The GC and the scheduler both sometimes need to stop a goroutine at a point where its stack and registers can be described exactly: which slots hold live pointers. Those are safe points.

  • Cooperative preemption (all versions): the runtime sets the goroutine's stack guard to a poison value, so the next function prologue's stack check fails and diverts into the scheduler. Function calls are therefore safe points.
  • Asynchronous preemption (Go 1.14+): sysmon notices a goroutine running for more than about 10 ms and sends its thread a signal (SIGURG on Unix; thread suspension on Windows). The signal handler checks that the interrupted instruction is an async safe point, then injects a call to asyncPreempt, which saves every register and scans the frame conservatively.
func main() {
    runtime.GOMAXPROCS(1)
    go func() {
        for {} // tight loop: no calls, no prologue checks
    }()
    time.Sleep(10 * time.Millisecond) // main yields; the spinner gets the P
    runtime.GC()                      // needs a STW
    fmt.Println("done")
}

Before Go 1.14 this program could hang forever: the spinning goroutine never reached a safe point, so the stop-the-world never completed and main never ran again. Since 1.14 it prints done.

Still not preemptible: code in //go:nosplit functions, some runtime and assembly sequences, and goroutines running cgo calls or blocked in syscalls. Those last two do not hold a P, so they do not block STW. GODEBUG=asyncpreemptoff=1 turns the signal mechanism off, which is sometimes used to debug signal-related issues (for example EINTR from badly written cgo libraries).

What the interviewer is looking for: you can connect preemption to GC latency: a single non-preemptible goroutine delays every STW phase, and therefore every goroutine.

More on Memory, GC & Runtime Internals

All 38 Memory, GC & Runtime Internals questions