---
title: "Go Scheduler, Yield Points, and Infinite Loops"
date: 2025-05-24
tags: ["go"]
description: "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:

```go
for {
    if condition {
        break
    }
}
```

versus

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

```mermaid
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

```mermaid
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
```

### Async Preemption

Since Go 1.14, `sysmon` detects goroutines running longer than ~10ms and forces preemption via SIGURG.

```mermaid
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 eventually
```

## Choosing the Right Pattern

```mermaid
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.
