— notes

Go Scheduler, Yield Points, and Infinite Loops

1 min read#go
On this page
  1. The GMP Model
  2. Cooperative Scheduling
  3. Async Preemption
  4. Choosing the Right Pattern

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"| LQ1

Cooperative 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 execution</code></pre></figure>

Async 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-&gt;&gt;G: Running for &gt;10ms (no yield points)
SM-&gt;&gt;SM: Detects long-running G
SM-&gt;&gt;G: Send SIGURG
Note over G: Signal handler fires, saves state
G-&gt;&gt;S: Suspended
S-&gt;&gt;S: Schedule next goroutine
S-&gt;&gt;G: Re-schedule eventually</code></pre></figure>

Choosing 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.