Work Stealing and findrunnable

Updated

September 8, 2026

Work Stealing and findrunnable

Overview

When a P needs work, it does not only look at its local run queue. The scheduler’s findrunnable path checks local queue, global queue, work stealing, netpoll, and idle logic. This is how Go keeps cores busy without a single giant lock.

Diagram: where is the next G?

  P needs a runnable G
       │
       v
  local runq? ──yes──► run it
       │ no
       v
  global runq? ──yes──► run it
       │ no
       v
  steal from other P? ──yes──► run it
       │ no
       v
  netpoll ready? ──yes──► run it
       │ no
       v
  idle / wait for work

Local vs global queues

Queue Property
Per-P local Fast, little contention
Global Overflow / balancing; more shared

New Gs often land local to the creator’s P; load imbalance triggers steal.

Work stealing

Idle P steals half (teaching model) of another P’s local queue when possible—classic multiprocessor scheduler trick.

P_idle looks at P_victim.runq
  → take some Gs
  → run them locally

Netpoll integration

Before declaring the machine idle, try non-blocking netpoll: “any FD ready?” Ready network Gs become runnable without a dedicated thread blocked in read.

sysmon also helps poll when all Ps are busy (213, 239).

Why you care

Symptom Scheduler angle
Runnable Gs, idle CPUs steal/netpoll delay, GOMAXPROCS, cgroup
Latency + low CPU parking on chan/IO, not missing steal
Syscall heavy extra Ms; P handoff (201)

Experiment

GODEBUG=schedtrace=1000 go run .
package main
import ("sync"; "time")
func main() {
    var wg sync.WaitGroup
    for i := 0; i < 1000; i++ {
        wg.Go(func() { time.Sleep(50 * time.Millisecond) })
    }
    wg.Wait()
}

What to notice: schedtrace lines show runqueue lengths and idle/spinning threads (format version-dependent).

Try next: Capture go tool trace and inspect Proc start/stop + Syscall blocks.