notes
Go Scheduler, Yield Points, and Infinite Loops
How Go's scheduler, yield points, and tight loops interact, and when a busy loop starves other goroutines.
I’ve been reviewing performance-critical code, and I keep coming back to this pattern:
for {
if condition {
break
}
}versus
go func() {
for {
// background processing
}
}()The runtime implications are fascinating.
The GMP Model
Go’s scheduler uses a GMP model: Goroutines (G) run on Machine threads (M) via Processors (P). Each P has a local run queue plus a global run queue.
graph TD
subgraph "Global Run Queue"
GQ["G5, G6, G7 ..."]
end
subgraph "P0 (Processor)"
LQ0["Local Queue:\nG1, G2"]
LQ0 --> M0["M0\n(OS Thread)"]
M0 --> CPU0["CPU Core 0"]
end
subgraph "P1 (Processor)"
LQ1["Local Queue:\nG3, G4"]
LQ1 --> M1["M1\n(OS Thread)"]
M1 --> CPU1["CPU Core 1"]
end
GQ -.->|"schedule"| LQ0
GQ -.->|"schedule"| LQ1
LQ0 -.->|"work steal"| LQ1Cooperative Scheduling
sequenceDiagram
participant S as Scheduler
participant A as Goroutine A
participant B as Goroutine B
S->>A: Schedule on P0
Note over A: Executing...
A->>A: Channel send (yield point)
A->>S: Yield control
S->>B: Schedule on P0
Note over B: Executing...
B->>B: runtime.Gosched()
B->>S: Yield control
S->>A: Re-schedule on P0
Note over A: Resumes executionAsync Preemption
Since Go 1.14, sysmon detects goroutines running longer than ~10ms and forces preemption via SIGURG.
sequenceDiagram
participant SM as sysmon
participant G as Goroutine (tight loop)
participant S as Scheduler
G->>G: Running for >10ms (no yield points)
SM->>SM: Detects long-running G
SM->>G: Send SIGURG
Note over G: Signal handler fires, saves state
G->>S: Suspended
S->>S: Schedule next goroutine
S->>G: Re-schedule eventuallyChoosing the Right Pattern
graph LR
A{"Need\nconcurrency?"}
A -->|No| B["for {} loop\n(direct execution)"]
A -->|Yes| C{"Bounded\nwork?"}
C -->|Yes| D["Worker Pool\n(fixed goroutines)"]
C -->|No| E{"I/O or\nCPU bound?"}
E -->|I/O| F["Goroutine per task\n(scheduler handles blocking)"]
E -->|CPU| G["select {} loop\n(controlled yielding)"]For most apps, use goroutines everywhere. For the 5% of performance-critical code, choose based on your scheduler profile.