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_UnsafePoint table), and if so rewrites the signal context to look like the thread just called asyncPreempt.

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:

go
// 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.

go
// 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 = stackPreempt

This 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:

go
// 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 = _SIGURG

preemptM is what sends it. It is called with the target goroutine’s scan bit held, and it releases that bit before returning:

go
// 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:

go
// 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:

go
// 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:

go
// 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:

go
// 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:

go
// 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.

go
// 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:

go
// 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:

go
// 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.

go
// 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:

asm
// 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:

go
// 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:

go
// 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:

  1. The GC. Before a GC cycle, the runtime asks every P to stop its goroutine so the mark phase can start.
  2. 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:
go
// src/runtime/proc.go:6689
// forcePreemptNS is the time slice given to a G before it is
// preempted.
const forcePreemptNS = 10 * 1000 * 1000 // 10ms
  1. You. runtime.Gosched() and the //go:yield pragma.

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:

go
// src/internal/goexperiment/exp_preemptibleloops_on.go:3
//go:build goexperiment.preemptibleloops

package goexperiment

const PreemptibleLoops = true
const PreemptibleLoopsInt = 1

and the compiler pass that does the work is enabled by that flag:

go
// 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:

go
// 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-spilling asyncPreempt trampoline.
  • src/runtime/proc.go — preemptone, preemptall, goschedImpl, gopreempt_m, preemptPark, and the 10 ms forcePreemptNS time slice.
  • src/internal/abi/symtab.go — PCDATA_UnsafePoint and the UnsafePoint* 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.

GoInternalsPreemptionSignalsConcurrency