Go: Goroutine, OS Thread and CPU Management
--
ℹ️ 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:
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:
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: