Go Preempts Goroutines with a Signal: Asynchronous Preemption Since Go 1.14
Since Go 1.14 the runtime can stop a goroutine anywhere in user code by sending it SIGURG, adjusting the signal context to look like the thread called asyncPreempt, and resuming it.
TL;DR: A goroutine that never calls anything has to be stoppable anyway — otherwise the garbage collector could never finish. Go used to rely on the goroutine hitting a function call, but since Go 1.14 the runtime sends the thread a SIGURG, checks whether the interrupted program counter is an async safe point (using the compiler’s
PCDATA_UnsafePointtable), and if so rewrites the signal context to look like the thread just calledasyncPreempt.
Preemption is the mechanism that lets Go have cheap goroutines. If a
goroutine could never be stopped, then a loop with no function calls would
hold the garbage collector hostage forever, and GOMAXPROCS would be a lie.
Go has two ways to stop a goroutine, and the runtime’s own header comment describes both:
// src/runtime/preempt.go:5
// Goroutine preemption
//
// A goroutine can be preempted at any safe-point. Currently, there
// are a few categories of safe-points:
//
// 1. A blocked safe-point occurs for the duration that a goroutine is
// descheduled, blocked on synchronization, or in a system call.
//
// 2. Synchronous safe-points occur when a running goroutine checks
// for a preemption request.
//
// 3. Asynchronous safe-points occur at any instruction in user code
// where the goroutine can be safely paused and a conservative
// stack and register scan can find stack roots. The runtime can
// stop a goroutine at an async safe-point using a signal.
//
// At both blocked and synchronous safe-points, a goroutine's CPU
// state is minimal and the garbage collector has complete information
// about its entire stack. This makes it possible to deschedule a
// goroutine with minimal space, and to precisely scan a goroutine's
// stack.
//
// Synchronous safe-points are implemented by overloading the stack
// bound check in function prologues. To preempt a goroutine at the
// next synchronous safe-point, the runtime poisons the goroutine's
// stack bound to a value that will cause the next stack bound check
// to fail and enter the stack growth implementation, which will
// detect that it was actually a preemption and redirect to preemption
// handling.
//
// Preemption at asynchronous safe-points is implemented by suspending
// the thread using an OS mechanism (e.g., signals) and inspecting its
// state to determine if the goroutine was at an asynchronous
// safe-point. Since the thread suspension itself is generally
// asynchronous, it also checks if the running goroutine wants to be
// preempted, since this could have changed. If all conditions are
// satisfied, it adjusts the signal context to make it look like the
// signaled thread just called asyncPreempt and resumes the thread.
// asyncPreempt spills all registers and enters the scheduler.Read that twice. The key sentence is the last one: the runtime does not
preempt in the signal handler. It adjusts the signal context so that, when
the thread resumes, it looks like the thread itself called asyncPreempt.
The synchronous path: poison the stack bound
The cheap way to preempt a goroutine is to make its next function prologue
fail. The runtime owns a field called stackguard0, which holds the stack
bound that every prologue compares against. Set it to a poison value and the
next call goes down the “stack overflow” path — where the runtime notices it
was actually a preemption request and handles it.
// src/runtime/proc.go:6929
func preemptone(pp *p) bool {
mp := pp.m.ptr()
if mp == nil || mp == getg().m {
return false
}
gp := mp.curg
if gp == nil || gp == mp.g0 {
return false
}
if readgstatus(gp)&^_Gscan == _Gsyscall {
// Don't bother trying to preempt a goroutine in a syscall.
return false
}
gp.preempt = true
// Every call in a goroutine checks for stack overflow by
// comparing the current stack pointer to gp->stackguard0.
// Setting gp->stackguard0 to StackPreempt folds
// preemption into the normal stack overflow check.
gp.stackguard0 = stackPreemptThis is free: no signal, no context switch. But it only works if the goroutine
ever calls another function. A tight loop like for i := 0; i < 1e9; i++ { total += i } has no prologue to trip, so the request would sit there forever.
That is exactly the case asynchronous preemption exists for.
The asynchronous path: SIGURG
The runtime picks a signal that nobody uses. signal_unix.go explains the
choice at length:
// src/runtime/signal_unix.go:64
// 4. We need to deal with platforms without real-time signals (like
// macOS), so those are out.
//
// We use SIGURG because it meets all of these criteria, is extremely
// unlikely to be used by an application for its "real" meaning (both
// because out-of-band data is basically unused and because SIGURG
// doesn't report which socket has the condition, making it pretty
// useless), and even if it is, the application has to be ready for
// spurious SIGURG. SIGIO wouldn't be a bad choice either, but is more
// likely to be used for real.
const sigPreempt = _SIGURGpreemptM is what sends it. It is called with the target goroutine’s scan
bit held, and it releases that bit before returning:
// src/runtime/signal_unix.go:363
// preemptM sends a preemption request to the M running gp. This request may
// be handled asynchronously and may be coalesced with other requests to
// the M. When the request is received, if the running G or P are
// marked for preemption and the goroutine is at an asynchronous
// safe-point, it will preempt the goroutine. It always atomically
// increments the M's preemptGen after handling a preemption request.
//
// gp.preempt must be set, and gp must be in _Gscanrunning, with its _Gscan
// bit held by the caller.
// preemptM releases the _Gscan bit before returning.
func preemptM(gp *g) {
// Save the M before releasing the _Gscan bit, which lets gp change Ms.
// preemptM is best effort, so it's fine if we signal a stale M.
mp := gp.m
casfrom_Gscanstatus(gp, _Gscanrunning, _Grunning)and then, at the bottom of the function:
// src/runtime/signal_unix.go:385
if mp.signalPending.CompareAndSwap(0, 1) {
if GOOS == "darwin" || GOOS == "ios" {
pendingPreemptSignals.Add(1)
}
// If multiple threads are preempting the same M, it may send many
// signals to the same M such that it hardly make progress, causing
// live-lock problem. Apparently this could happen on darwin. See
// issue #37741.
// Only send a signal if there isn't already one pending.
signalM(mp, sigPreempt)
}The signalPending flag means one signal per M at a time. Ten threads all
asking the same M to stop should not deliver ten signals.
On Linux, “send a signal to this M” means tgkill, i.e. the kill syscall
with a thread id:
// src/runtime/os_linux.go:579
// signalM sends a signal to mp.
func signalM(mp *m, sig int) {
tgkill(getpid(), int(mp.procid), sig)
}The signal handler
The signal arrives on the thread the runtime wants to stop. The handler runs
on the gsignal stack, and the interesting part is doSigPreempt:
// src/runtime/signal_unix.go:341
// doSigPreempt handles a preemption signal on gp.
func doSigPreempt(gp *g, ctxt *sigctxt) {
// Check if this G wants to be preempted and is safe to
// preempt.
if wantAsyncPreempt(gp) {
if ok, newpc := isAsyncSafePoint(gp, ctxt.sigpc(), ctxt.sigsp(), ctxt.siglr()); ok {
// Adjust the PC and inject a call to asyncPreempt.
ctxt.pushCall(abi.FuncPCABI0(asyncPreempt), newpc)
}
}
// Acknowledge the preemption.
gp.m.preemptGen.Add(1)
gp.m.signalPending.Store(0)
if GOOS == "darwin" || GOOS == "ios" {
pendingPreemptSignals.Add(-1)
}
}pushCall is the trick. On amd64 it is four instructions’ worth of logic:
// src/runtime/signal_amd64.go:80
func (c *sigctxt) pushCall(targetPC, resumePC uintptr) {
// Make it look like we called target at resumePC.
sp := uintptr(c.rsp())
sp -= goarch.PtrSize
*(*uintptr)(unsafe.Pointer(sp)) = resumePC
c.set_rsp(uint64(sp))
c.set_rip(uint64(targetPC))
}Push the resume PC onto the thread’s stack, point the instruction pointer at
asyncPreempt, and let the thread continue. When asyncPreempt returns, the
ret will pop the resume PC and the goroutine continues exactly where it
was — except that it has been through the scheduler.
Note wantAsyncPreempt is checked first. The signal may have been delivered
late, or coalesced with something else, so the handler re-reads the current
state rather than trusting the request:
// src/runtime/preempt.go:375
// wantAsyncPreempt returns whether an asynchronous preemption is
// queued for gp.
func wantAsyncPreempt(gp *g) bool {
// Check both the G and the P.
return (gp.preempt || gp.m.p != 0 && gp.m.p.ptr().preempt) && readgstatus(gp)&^_Gscan == _Grunning
}Deciding whether it is safe: isAsyncSafePoint
Not every instruction can be interrupted. If the thread is halfway through a write barrier, or inside a runtime function, stopping it and conservatively scanning the stack would be wrong — or would deadlock.
// src/runtime/preempt.go:398
func isAsyncSafePoint(gp *g, pc, sp, lr uintptr) (bool, uintptr) {
mp := gp.m
// Only user Gs can have safe-points. We check this first
// because it's extremely common that we'll catch mp in the
// scheduler processing this G preemption.
if mp.curg != gp {
return false, 0
}
// Check M state.
if mp.p == 0 || !canPreemptM(mp) {
return false, 0
}
// Check stack space.
if sp < gp.stack.lo || sp-gp.stack.lo < asyncPreemptStack {
return false, 0
}and then the interesting part, the compiler-provided table:
// src/runtime/preempt.go:451
up, startpc := pcdatavalue2(f, abi.PCDATA_UnsafePoint, pc)
if up == abi.UnsafePointUnsafe {
// Unsafe-point marked by compiler. This includes
// atomic sequences (e.g., write barrier) and nosplit
// functions (except at calls).
return false, 0
}
if fd := funcdata(f, abi.FUNCDATA_LocalsPointerMaps); fd == nil || f.flag&abi.FuncFlagAsm != 0 {
// This is assembly code. Don't assume it's well-formed.
// TODO: Empirically we still need the fd == nil check. Why?
//
// TODO: Are there cases that are safe but don't have a
// locals pointer map, like empty frame functions?
// It might be possible to preempt any assembly functions
// except the ones that have funcFlag_SPWRITE set in f.flag.
return false, 0
}PCDATA_UnsafePoint is a per-function table mapping program counters to
safety. The encoding lives in internal/abi:
// src/internal/abi/symtab.go:124
// Special values for the PCDATA_UnsafePoint table.
const (
UnsafePointSafe = -1 // Safe for async preemption
UnsafePointUnsafe = -2 // Unsafe for async preemption
// UnsafePointRestart1(2) apply on a sequence of instructions, within
// which if an async preemption happens, we should back off the PC
// to the start of the sequence when resuming.
// We need two so we can distinguish the start/end of the sequence
// in case that two sequences are next to each other.
UnsafePointRestart1 = -3
UnsafePointRestart2 = -4
// Like UnsafePointRestart1, but back to function entry if async preempted.
UnsafePointRestartAtEntry = -5
)UnsafePointRestart1/Restart2 are how the compiler marks a restartable
instruction sequence: if you are preempted in the middle of it, the PC is
rewound to the start of the sequence so the whole thing can be redone. That
is how the runtime can safely interrupt, say, a multi-word store.
There is also a blunt instrument: the runtime never preempts its own code.
// src/runtime/preempt.go:468
// Check the inner-most name
u, uf := newInlineUnwinder(f, pc)
name := u.srcFunc(uf).name()
if stringslite.HasPrefix(name, "runtime.") ||
stringslite.HasPrefix(name, "internal/runtime/") ||
stringslite.HasPrefix(name, "reflect.") {
// For now we never async preempt the runtime or
// anything closely tied to the runtime. Known issues
// include: various points in the scheduler ("don't
// preempt between here and here"), much of the defer
// implementation (untyped info on stack), bulk write
// barriers (write barrier check), atomic functions in
// internal/runtime/atomic, reflect.{makeFuncStub,methodValueCall}.asyncPreempt: spilling the registers
Once the thread resumes, it is running asyncPreempt, which is assembly. On
amd64 it saves every general-purpose register onto the goroutine’s stack,
because any of them might hold a pointer:
// src/runtime/preempt_amd64.s:8
TEXT ·asyncPreempt(SB),NOSPLIT|NOFRAME,$0-0
PUSHQ BP
MOVQ SP, BP
// Save flags before clobbering them
PUSHFQ
// obj doesn't understand ADD/SUB on SP, but does understand ADJSP
ADJSP $112
// But vet doesn't know ADJSP, so suppress vet stack checking
NOP SP
// Save GPs
MOVQ AX, 0(SP)
MOVQ CX, 8(SP)
MOVQ DX, 16(SP)
MOVQ BX, 24(SP)
MOVQ SI, 32(SP)
MOVQ DI, 40(SP)
MOVQ R8, 48(SP)
MOVQ R9, 56(SP)
MOVQ R10, 64(SP)
MOVQ R11, 72(SP)
MOVQ R12, 80(SP)
MOVQ R13, 88(SP)
MOVQ R14, 96(SP)
MOVQ R15, 104(SP)The Go continuation is deliberately nosplit and write-barrier free:
// src/runtime/preempt.go:315
// asyncPreempt2 is the Go continuation of asyncPreempt.
//
// It must be deeply nosplit because there's untyped data on the stack from
// asyncPreempt.
//
// It must not have any write barriers because we need to limit the amount of
// stack it uses.
//
//go:nosplit
//go:nowritebarrierrec
func asyncPreempt2() {
// We can't grow the stack with untyped data from asyncPreempt, so switch to
// the system stack right away.
mcall(func(gp *g) {
gp.asyncSafePoint = true
// Move the extended register state from the P to the G. We do this now that
// we're on the system stack to avoid stack splits.
xRegSave(gp)
if gp.preemptStop {
preemptPark(gp)
} else {
gopreempt_m(gp)
}
// The above functions never return.
})The asyncSafePoint flag matters later: it tells the GC that this goroutine’s
stack was scanned conservatively, so the stack cannot be shrunk while it is
in that state.
After the preemption
gopreempt_m ends up in goschedImpl, which is the same path as
runtime.Gosched() except for the trace event and one detail:
// src/runtime/proc.go:4328
func goschedImpl(gp *g, preempted bool) {
pp := gp.m.p.ptr()
trace := traceAcquire()
status := readgstatus(gp)
if status&^_Gscan != _Grunning {
dumpgstatus(gp)
throw("bad g status")
}
if trace.ok() {
// Trace the event before the transition. It may take a
// stack trace, but we won't own the stack after the
// transition anymore.
if preempted {
trace.GoPreempt()
} else {
trace.GoSched()
}
}
casgstatus(gp, _Grunning, _Grunnable)Note that a preempted goroutine goes onto the global run queue, not the local one (unless the GC is waiting for it). That keeps the preempted goroutine from immediately being rescheduled onto the same P, which would make preemption useless for latency purposes.
What triggers a preemption?
Three things:
- The GC. Before a GC cycle, the runtime asks every P to stop its goroutine so the mark phase can start.
sysmon. The background thread forces a preemption if a P has been running the same goroutine for too long. The time slice is 10 ms:
// src/runtime/proc.go:6689
// forcePreemptNS is the time slice given to a G before it is
// preempted.
const forcePreemptNS = 10 * 1000 * 1000 // 10ms- You.
runtime.Gosched()and the//go:yieldpragma.
The GC case is the one that makes asynchronous preemption non-negotiable: a goroutine in a tight loop with no calls would never reach a safe point, and the GC would never finish. With SIGURG, the loop is interrupted at the next instruction the compiler marked safe.
The preemptible-loops experiment
There is a goexperiment.preemptibleloops build tag in the tree, off by
default. It makes the compiler insert an explicit rescheduling check on every
loop backedge:
// src/internal/goexperiment/exp_preemptibleloops_on.go:3
//go:build goexperiment.preemptibleloops
package goexperiment
const PreemptibleLoops = true
const PreemptibleLoopsInt = 1and the compiler pass that does the work is enabled by that flag:
// src/cmd/compile/internal/ssacompile/compile.go:449
{Name: "insert resched checks", Fn: insertLoopReschedChecks,
Disabled: !buildcfg.Experiment.PreemptibleLoops}, // insert resched checks in loops.The idea is the same as the synchronous preemption trick, applied to loops:
compare the stack pointer against the stack bound at the bottom of the loop,
and if the bound has been poisoned, call morestack — which the runtime then
recognises as a preemption request. It trades a comparison and a branch per
loop iteration for cheaper, more deterministic preemption. The compiler’s own
comment describes the mechanism:
// src/cmd/compile/internal/ssacompile/loopreschedchecks.go:47
// insertLoopReschedChecks inserts rescheduling checks on loop backedges.
func insertLoopReschedChecks(f *ssa.Func) {
// TODO: when split information is recorded in export data, insert checks only on backedges that can be reached on a split-call-free path.
// Loop reschedule checks compare the stack pointer with
// the per-g stack bound. If the pointer appears invalid,
// that means a reschedule check is needed.Trade-offs
What asynchronous preemption buys you. A goroutine in a loop with no
function calls is still stoppable. Before Go 1.14 it was not: the classic
“busy loop blocks the GC” bug was real, and the only fixes were to add a
function call or to call runtime.Gosched().
What it costs. A signal, a signal handler, a register spill, and a
conservative stack scan. Signals are not cheap: on Linux, tgkill is a
syscall, and the delivery of a signal to a thread that is running takes a
kernel round trip. The runtime limits how often this can happen per M with
the signalPending flag and a rate limit in suspendG.
Conservative scanning. At an async safe point the runtime does not know
precisely which words in the stack are pointers, so it scans conservatively.
That is why gp.asyncSafePoint is set, and why the runtime refuses to shrink
such a stack afterwards.
The runtime never preempts itself. Any function whose name starts with
runtime. or reflect. is off limits, which rules out a whole class of
bugs but also means a long loop inside the runtime cannot be interrupted.
Caveats
A goroutine blocked in a syscall cannot be preempted. preemptone gives
up explicitly when the target is in _Gsyscall. The runtime’s answer there
is to take the P away from the thread rather than stop the goroutine.
runtime.LockOSThread does not prevent preemption — it prevents the
goroutine from moving to another thread, but the goroutine can still be
stopped and rescheduled on the same thread.
Assembly functions are not preemptible. isAsyncSafePoint rejects
anything without a locals pointer map, and anything marked SPWRITE.
A cgo call blocks preemption for the duration of the C call. The runtime cannot interrupt arbitrary C code, and it will not try.
The preemptible-loops experiment is off by default. If you build with
GOEXPERIMENT=preemptibleloops you get loop-backedge checks, and the
compiler pass is still marked as work in progress.
GODEBUG=asyncpreemptoff=1 disables the whole mechanism, which is a
useful bisection tool when you suspect a preemption bug.
Wrapping up
The one thing to remember: Go has two preemption mechanisms, and the
asynchronous one is the interesting one. The runtime sends SIGURG to the
target thread, checks the interrupted PC against the compiler’s
PCDATA_UnsafePoint table, and if the spot is safe, rewrites the signal
context so the thread resumes inside asyncPreempt — which spills the
registers and enters the scheduler.
The part people get wrong: the preemption does not happen in the signal handler. The handler only adjusts the signal context. The goroutine is stopped by ordinary code resuming at a different program counter, which is why the runtime can use the same machinery for the stack-safety check it already had.
What to go do about it: if you have a latency-sensitive service and you
suspect GC pauses or scheduler latency, read the GODEBUG=schedtrace output
together with GODEBUG=asyncpreemptoff=1 runs; and if you are writing
assembly or using //go:nosplit, remember that you are opting out of
asynchronous preemption entirely.
Further reading
src/runtime/preempt.go— the whole design: safe-point categories,suspendG,isAsyncSafePoint,asyncPreempt2.src/runtime/signal_unix.go—sigPreempt,preemptM,doSigPreempt,sighandler.src/runtime/signal_amd64.go—sigctxt.pushCall, the signal-context rewriting.src/runtime/preempt_amd64.s— the register-spillingasyncPreempttrampoline.src/runtime/proc.go—preemptone,preemptall,goschedImpl,gopreempt_m,preemptPark, and the 10 msforcePreemptNStime slice.src/internal/abi/symtab.go—PCDATA_UnsafePointand theUnsafePoint*encodings.src/internal/goexperiment/exp_preemptibleloops_on.go— the preemptible-loops experiment flag.src/cmd/compile/internal/ssacompile/loopreschedchecks.go— the compiler pass that inserts loop reschedule checks.src/runtime/traceback.go— how the runtime reconstructs a goroutine’s stack when it is not running.