Location>code7788 >text

Go runtime scheduler in a nutshell (VII): case study

Popularity:733 ℃/2024-09-19 11:32:13

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 goroutineThe 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.