Work Stealing and findrunnable
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.
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.