0. Preface
We've spent the last six lectures introducing the Go runtime scheduler. In this lecture, we'll look at a case study of a program that schedules a goroutine and analyze what the scheduler does. It should be noted that this program is related to preemption, which has not been introduced so far, so it doesn't matter if you can't understand it, you just need to have an impression.
1. Case 1
Execute the code:
func gpm() {
var x int
for {
x++
}
}
func main() {
var x int
threads := (0)
for i := 0; i < threads; i++ {
go gpm()
}
(1 * )
("x = ", x)
}
Run the program:
# go run
x = 0
(Why x=0 is not relevant to this series, so we'll skip it here.)
Go in1.14 version introduces an asynchronous preemption mechanism, we use the1.21.0
version of Go, asynchronous preemption is turned on by default. Asynchronous preemption is enabled via theasyncpreemptoff
flag enables/disables asynchronous preemption.asyncpreemptoff=1
indicates that asynchronous preemption is disabled, and the correspondingasyncpreemptoff=0
Indicates that asynchronous preemption is turned on.
1.1 Disable asynchronous preemption
First, disable asynchronous preemption and execute the above code again:
# GODEBUG=asyncpreemptoff=1 go run
The program is stuck with no output. Check CPU utilization:
top - 10:08:53 up 86 days, 10:48, 0 users, load average: 3.08, 1.29, 0.56
Tasks: 179 total, 2 running, 177 sleeping, 0 stopped, 0 zombie
%Cpu(s): 74.4 us, 0.6 sy, 0.0 ni, 25.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
MiB Mem : 20074.9 total, 4279.4 free, 3118.3 used, 12677.2 buff/cache
MiB Swap: 0.0 total, 0.0 free, 0.0 used. 16781.0 avail Mem
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
1014008 root 20 0 1226288 944 668 R 293.7 0.0 5:35.81 main // main is the executing process
CPU occupancy up to293.7
, it's too high.
Why is this happening? We can find out by going to theGODEBUG=schedtrace=1000,scheddetail=1,asyncpreemptoff=1 Print G, P, and M information about program execution, and see what happened during scheduling with the DEBUG output.
When creating a number of threads equal to the number ofgoroutine
After the thread executesmain goroutine
The runtime (actually the sysmon thread, more on that later) realizes that the main goroutine is taking too long to run, and schedules it away to run other goroutines (this is active scheduling logic, not asynchronous preemption). Then it executes goroutines equal to the number of threads. These goroutines never exit, and the threads will keep executing, filling up the logic core.
To solve this problem, we change the code as follows:
func main() {
var x int
threads := (0)
for i := 0; i < threads; i++ {
go gpm()
}
(1 * )
("x = ", x)
}
Because the main goroutine is running too long, it is being dispatched away by runtime. Let's set the sleep time to 1 nanosecond to keep it from sleeping that long. Then we execute the program:
# GODEBUG=asyncpreemptoff=1 go run
x = 0
Program exit. The only way to do this is to do it quickly. The main goroutine exits directly after execution, without giving the runtime a chance to react.
Is there any other way to change it? Let's add to gpm function call:
func gpm() {
var x int
for {
(1 * )
x++
}
}
func main() {
var x int
threads := (0)
for i := 0; i < threads; i++ {
go gpm()
}
(1 * )
("x = ", x)
}
Run the program:
# GODEBUG=asyncpreemptoff=1 go run
x = 0
It also exits normally. Why is it possible to add a function call? It has to do with the preemption logic, because with a function call, you have the opportunity to set a "preemption flag" in the function's preamble, and perform the scheduling of the preempted goroutine (again, more on this later).
Watch this space.(1 * )
The location of the addition, if it is added here:
func gpm() {
var x int
(1 * )
for {
x++
}
}
The program still gets stuck.
We've been talking about it for half a day.asyncpreemptoff=1
The case for disabling asynchronous preemption. It's time to turn on asynchronous preemption to see the output.
1.2 Enabling asynchronous preemption
The program is still the same:
func gpm() {
var x int
for {
x++
}
}
func main() {
var x int
threads := (0)
for i := 0; i < threads; i++ {
go gpm()
}
(1 * )
("x = ", x)
}
Turn on asynchronous preemption execution:
# GODEBUG=asyncpreemptoff=0 go run
x = 0
Asynchronous preemption is fine. Why is asynchronous preemption fine? Asynchronous preemption signals a thread to execute the logic of asynchronous preemption at a "safe point" (the logic of asynchronous preemption will be introduced in the next few lectures).
Rewrite the code again as follows:
//go:nosplit
func gpm() {
var x int
for {
x++
}
}
func main() {
var x int
threads := (0)
for i := 0; i < threads; i++ {
go gpm()
}
(1 * )
("x = ", x)
}
The same execution output:
# GODEBUG=asyncpreemptoff=0 go run
The program is stuck again...
This program serves as a thought-provoking question as to why adding a//go:nosplit
And the program got stuck?
2. Summary
This talk is not intended to round out the word count, but mainly to set the stage for the introduction of the subsequent preemption, which will be introduced in the next talk as preemptive scheduling with excessively long runtimes.