Go path · Many things at once · Topic 9 of 13

Goroutines and the scheduler

Not started

A goroutine is a function running independently, started with go f(). It is not an operating-system thread: the runtime multiplexes many goroutines onto a few threads, and starting one costs a few kilobytes of stack and a little bookkeeping. The scheduler that does this is described at the top of runtime/proc.go with three letters: **G** for a goroutine, **M** for a machine (an OS thread), and **P** for a processor, the resource a thread must hold to run Go code.

Why it matters: Goroutines are cheap enough to start one per request, but they are not free and not magic. The scheduler model explains GOMAXPROCS, blocking system calls, and why a tight loop no longer freezes a program.

  1. 1G, M and P

    A G is a goroutine, an M is an operating-system thread, and a P is the right to run Go code. GOMAXPROCS sets the number of Ps.

  2. 2Run queues and stealing

    Each P has a local queue of runnable goroutines. An idle P steals work from the others, so load spreads without a central lock.

  3. 3Blocking doesn't waste a P

    Network waits park the goroutine on the netpoller. A blocking system call hands the P to another thread so other goroutines keep running.

  4. 4Preemption since Go 1.14

    The runtime can interrupt a goroutine even in a loop without function calls, so one busy goroutine can't starve the rest.

1

How it works

The explanation, with examples you can run.

The G-M-P model

  • There are exactly GOMAXPROCS Ps. Each P has a local run queue of goroutines ready to run, and there is also a global queue.
  • An M picks up a P and runs goroutines from its queue. When a goroutine blocks, the M moves on to the next one.
  • A P whose queue is empty steals half of another P's queue. Work spreads without one central lock.
  • go f() puts the new G on the current P's queue, so a burst of goroutines starts close to its parent.

GOMAXPROCS used to default to the number of logical CPUs. Since Go 1.25, on Linux it also respects the container's cgroup CPU limit and is updated if that limit changes, which stops Go services in Kubernetes from creating far more running threads than their CPU quota allows. Setting GOMAXPROCS yourself turns this off.

When a goroutine blocks

  • Channels, locks, sleeps. The goroutine is parked on that object and costs nothing until it is woken.
  • Network I/O. Sockets are non-blocking underneath. A goroutine that waits for data is parked on the netpoller (epoll, kqueue or IOCP), and the runtime makes it runnable when the socket is ready. Ten thousand idle connections are ten thousand parked goroutines, not ten thousand blocked threads.
  • Blocking system calls and cgo. File reads on many systems, and C code, really do block the thread. The runtime detaches the P from that thread and gives it to another M, starting a new thread if needed. That is why a Go program can have more threads than GOMAXPROCS.

Preemption

Goroutines used to switch only at function calls and blocking operations, so a tight loop with no calls could hold a P forever and even delay garbage collection. Since Go 1.14 goroutines are asynchronously preemptible: on most platforms the runtime sends the thread a signal and switches goroutines at a safe point.

package main import (	"fmt"	"runtime"	"time") func main() {	runtime.GOMAXPROCS(1) // one P: only one goroutine runs Go code at a time	go func() {		for { // a tight loop with no function calls		}	}()	time.Sleep(10 * time.Millisecond) // the spinning goroutine gets the P	fmt.Println("main still gets to run") // because the runtime preempts the loop}

On Go 1.26 this prints its line and exits. Scheduling is fair enough, but not in any guaranteed order: never use sleeps or the order goroutines were started as synchronisation.

Cheap, but not free

Each goroutine holds its stack and everything it references until it returns. A goroutine blocked forever on a channel nobody will send to is a leak: memory and often a socket. Every go statement should have an answer to "how does this end?", usually a closed channel or a cancelled context. Go 1.27 made a goroutineleak profile generally available (in runtime/pprof and at /debug/pprof/goroutineleak); it uses the garbage collector to find goroutines blocked on something no running goroutine can reach.

2

Check yourself

3 questions. Get them all right to finish this topic.

  1. 1.What does GOMAXPROCS limit?
  2. 2.Since Go 1.25, what is the default GOMAXPROCS in a Linux container limited to 2 CPUs on a 64-core host?
  3. 3.With GOMAXPROCS(1), one goroutine spins in for {} with no function calls. Does main still get to run?

0 of 3 right so far. Get all three to finish this topic.