Who gets the P when a goroutine blocks in a syscall?

Read the explanation
What if

main blocks in read() on a plain pipe. Its OS thread waits in the kernel, and Go hands the P to another thread so the ticker keeps running.

Predict first

One P. main blocks in read() on a plain pipe for 50 ms. Can the ticker run meanwhile?

012345678910thread 1: main: pipe, go ticker()mainmain: inside read(), in the kernelthread 2: ticker: ticktickthread 2: ticker: ticktickthread 2: writer: write()writemain: print true

Tap any block to see what that thread is doing.

  • Running
  • Waiting on I/O
Step 1/5

Lanes are OS threads here, not goroutines. With one P, only the thread holding it runs Go code.

1var ticks atomic.Int642 3func ticker() {4    for {5        ticks.Add(1)6        time.Sleep(time.Millisecond)7    }8}9 10func main() {11    runtime.GOMAXPROCS(1) // one P: one goroutine runs Go code at a time12    var p [2]int13    if err := syscall.Pipe(p[:]); err != nil { // a plain pipe: reading it blocks in the kernel14        panic(err)15    }16    go ticker()17    go func() {18        time.Sleep(50 * time.Millisecond)19        syscall.Write(p[1], []byte("x"))20    }()21    before := ticks.Load()22    buf := make([]byte, 1)23    syscall.Read(p[0], buf) // main's thread waits inside read() until the write24    fmt.Println("ticker ran during the read:", ticks.Load() > before)25}

Explanation

What just happened

With GOMAXPROCS(1) there is one P, and only the thread holding it runs Go code. main called read() on a plain pipe, and its thread went into the kernel to wait.

Go didn’t let that stall everything. It handed the P to another thread, and the ticker and the writer ran there. When read() returned, main got a P back and carried on on its own thread.

Try the what-if switch

Why it works this way

A thread in a system call can’t run Go code, but the P can. Handing it over keeps the other goroutines moving. Network I/O goes further: the netpoller watches the socket, so a waiting read holds no thread at all.

Watch out

Every goroutine blocked in a system call holds its own OS thread. Many of them at once means many threads.

Common questions

What happens when a goroutine makes a blocking system call?

Its OS thread waits in the kernel, and the goroutine dump shows [syscall]. Go hands the P to another thread, so other goroutines keep running: here the ticker ran on a second thread the whole time.

Does a network read block an OS thread in Go?

No. A conn.Read with no data parks the goroutine ([IO wait]) and its thread runs other goroutines. 20 goroutines waiting on network reads at once didn't need 20 threads.

Why does my Go program have so many threads?

Goroutines blocked in system calls each hold an OS thread. 20 goroutines blocked in syscall.Read needed 20 threads; 20 waiting on network reads didn't.

Verified: this timeline matches a real program, checked in CI on Go 1.26 and 1.27. How we verify. Unclear or wrong? Tell us