Go 1.14 lets the scheduler interrupt busy loops
Go 1.14 removes a scheduling dependency on function calls. Its runtime checks where a goroutine stopped before injecting a call that saves registers and enters the scheduler.
A loop that performs useful computation without calling another function should not prevent unrelated goroutines from running. Before Go 1.14, such a loop could keep the runtime waiting for a cooperative preemption point. The consequences included scheduler stalls and delayed garbage collection. Today's release adds asynchronous goroutine preemption, so the runtime can interrupt suitable execution points inside that loop.
The Go 1.14 announcement includes this change alongside faster deferred calls and production-ready module support. The scheduling work deserves a closer look because it changes how much the runtime depends on the shape of ordinary application code. A programmer should not need to preserve an otherwise unnecessary function call merely to let the scheduler regain control.
I value Go's ability to keep code legible without requiring each author to invent operational conventions. Moving this responsibility into the runtime fits that preference. Understanding the implementation still matters when code interacts with signals, assembly, or low-level system calls.
Why function calls mattered
The tagged preemption implementation explains the existing synchronous mechanism. Function prologues check whether enough stack space remains. To request preemption, the runtime changes the goroutine's stack bound so that its next check enters the stack-growth machinery. That machinery recognizes the preemption request and transfers execution to the appropriate runtime path.
This design reuses a check the program already needs. It also means that a running goroutine must reach such a check before the request takes effect. A computation that remains inside a loop without calls can avoid the relevant prologue indefinitely. The loop does not need an application-level deadlock to obstruct the scheduler. Its generated instructions can simply lack the point at which the runtime expects cooperation.
Blocked goroutines have a different situation. A goroutine waiting for synchronization or sitting in a system call is already at what the runtime describes as a blocked safe point. Its execution state is sufficiently constrained for the runtime to inspect it. The difficult case is a goroutine actively executing user code while another part of the runtime needs it to stop.
The garbage collector adds another requirement. Pausing execution is useful only if the runtime can identify the pointers that keep objects alive. A stop at an arbitrary machine instruction may find a partially completed operation or pointer state that ordinary stack metadata cannot describe. Asynchronous preemption therefore needs both an interruption mechanism and a way to decide whether the interruption occurred at a usable location.
Interrupting without stopping anywhere
On supported systems, Go 1.14 uses an operating-system mechanism to interrupt the thread running a goroutine. Signals provide that mechanism on Unix. The runtime examines the interrupted execution state and checks whether the goroutine still needs preemption. This second check is necessary because the request and the interruption do not happen at the same instant.
If the location is safe, the runtime changes the saved execution context so that resuming the thread behaves as though it had called asyncPreempt. That function saves the user registers and enters the scheduler through a runtime helper. Saving registers gives stack scanning access to values that had not been written into ordinary stack slots.
The implementation deliberately avoids running the scheduler inside the signal handler. Its comments explain that doing so would retain an operating-system thread for each preempted goroutine. The scheduler also allocates memory and starts threads along paths that are unsuitable for direct execution in a signal handler. Injecting the call lets the signal handler return before ordinary scheduler work proceeds.
This makes the meaning of asynchronous fairly precise. The application does not have to execute a voluntary preemption check at a function entry. However, the runtime still accepts only locations that meet its safety conditions. A request to interrupt execution does not authorize the runtime to suspend halfway through an operation whose invariants require completion.
What makes a location safe
The isAsyncSafePoint function checks several properties before approving a location. It verifies that the interrupted goroutine is the thread's current user goroutine. It checks that the thread has a processor available and is in a state the runtime can preempt. Runtime locks, allocation activity, and explicit preemption restrictions can all rule that state out.
Available stack space matters too. The runtime needs room to inject the call and save the registers. Initialization computes the stack requirement for the preemption functions, including additional return-address overhead. The safety check rejects a stack pointer outside the expected bounds or too close to the bottom of the goroutine's stack.
Compiler metadata identifies instructions that cannot be asynchronous safe points. The source mentions atomic sequences such as write barriers, where interruption could expose an incomplete operation. Assembly functions without the expected pointer maps also fail the check. Knowing that an instruction belongs to executable Go code is insufficient without the information needed to scan its state.
Go 1.14 also excludes runtime code and closely related implementation code from this asynchronous path. The source names scheduler sequences and parts of deferred-call handling among the reasons. These restrictions explain why a concise release-note statement about preemptible goroutines rests on substantial cooperation between generated code, stack metadata, and the runtime.
For an application author, the practical result is that a busy computation need not call a helper solely to offer the scheduler an entry point. It does not mean that every instruction in every linked function becomes interruptible. The implementation makes the decision using the actual stopped location and current runtime state.
Extra signals reach system-call code
The release notes warn that Unix programs built with Go 1.14 receive more signals than programs built with earlier releases. Code using syscall or golang.org/x/sys/unix can consequently see more slow system calls return EINTR. Such code must handle interruption, commonly by retrying the operation where that is appropriate for the system call.
This is a concrete integration cost of the scheduling change. Application code that interprets every low-level call failure as a final failure may expose errors that its previous execution pattern rarely produced. The extra signal does not imply that the remote endpoint or device failed. It means the system call returned after an interruption, and the wrapper must apply that call's interruption semantics.
A universal retry wrapper would conceal too much. Different operations have different rules about partial progress and whether the interrupted operation already changed state. The useful boundary is the low-level wrapper that knows the particular system call. Higher application layers should receive the result after that wrapper has applied the platform's documented behavior.
Platform coverage also limits the claim. The release notes exclude windows/arm, darwin/arm, js/wasm, and all Plan 9 targets from asynchronous preemption. Describing this as an unconditional property of every Go 1.14 binary would be inaccurate. The supported implementation removes the busy-loop problem where the necessary platform machinery exists.
Deferred calls become cheaper too
Go 1.14 also reduces the cost of most uses of defer to nearly that of calling the deferred function directly. The release notes explicitly connect this improvement with using deferred calls in performance-sensitive code. That matters because cleanup written beside acquisition is easier to inspect than cleanup duplicated across several return paths.
The qualification about most uses remains part of the claim. The announcement does not promise that every placement of a deferred call has identical generated code or cost. It establishes that the common performance objection has changed substantially. Decisions about cleanup can give more weight to whether the control flow is clear.
I see the preemption and defer work as improvements to the cost of writing straightforward Go. They reduce reasons to reshape ordinary code around implementation details. Neither requires an application-facing language construct for the benefit to appear when a program is rebuilt.
The runtime also changes its page allocator to reduce contention at high GOMAXPROCS values. Internal timer work reduces contention and context switches for operations such as connection deadlines. These changes address coordination costs in the runtime itself, which is a different concern from making an individual application function compute faster.
For scheduling, the important implementation boundary remains the safe-point decision. Go 1.14 gains control inside suitable call-free loops, saves the interrupted state, and transfers execution through the scheduler. Code that wraps low-level Unix calls must account for interruption, while supported ordinary Go computations can make progress without manufacturing function calls for preemption.