Go: Goroutine, OS Thread and CPU Management

Vincent Blanchon
A Journey With Go
Published in
5 min readNov 20, 2019

Illustration created for “A Journey With Go”, made from the original Go Gopher, created by Renee French.

ℹ️ This article is based on Go 1.13.

Creating an OS Thread or switching from one to another can be costly for your programs in terms of memory and performance. Go aims to get advantages as much as possible from the cores. It has been designed with concurrency in mind from the beginning.

M, P, G orchestration

To solve this problem, Go has its own scheduler to distribute goroutines over the threads. This scheduler defines three main concepts, as explained in the code itself:

The main concepts are:
G - goroutine.
M - worker thread, or machine.
P - processor, a resource that is required to execute Go code.
M must have an associated P to execute Go code[...].

Here is a diagram of this P, M, G model:

Each goroutine (G) runs on an OS thread (M) that is assigned to a logical CPU (P). Let’s take a simple example to see how Go manages them:

func main() {
var wg sync.WaitGroup
wg.Add(2)

go func() {
println(`hello`)
wg.Done()
}()

go func() {
println(`world`)
wg.Done()
}()

wg.Wait()
}

Go will first create the different P based on the number of logical CPUs of the machine and store them in a list of idle P:

Then, the new goroutine or goroutines ready to run will wake a P up to distribute the work better. This P will create an M with the associated OS thread:

OS thread creation

However, like a P, a M with no work — i.e. no goroutine waiting to run — returning from a syscall, or even forced to be stopped by the garbage collector, goes to an idle list:

M and P idle list

During the bootstrap of the program, Go already creates some OS thread and associated M. For our example, the first goroutine that prints hello will use the main goroutine while the second one will get an M and P from this idle list: