0. Preface
existGo runtime scheduler in a nutshell (3): main goroutine creation We introduced the creation of a main goroutine, and said that there is a difference between a main goroutine and a non-main goroutine. We didn't get to the bottom of that, but we'll move on to the running of non-main goroutines (that is, goroutines created by the go keyword, henceforth referred to as gp), and we'll unpack that, and talk about the differences between them.
1. gp creation
First look at an example:
func g2() {
(10 * )
println("hello world")
}
func main() {
go g2()
(1 * )
println("main exit")
}
The main function creates two goroutines, a main goroutine and a normal goroutine, from theGo runtime scheduler in a nutshell (4): running the main goroutine It can be seen that after the main goroutine has run, it calls theexit(0)
Exit. Here we have added a 1 minute wait time to the main goroutine in order to be able to enter gp.
The startup of Go runtime was covered in the previous lectures, so let's go straight to the main function and see how gp is created:
(dlv) c
> () ./:12 (hits goroutine(1):1 total:1) (PC: 0x46238a)
7: func g2() {
8: (10 * )
9: println("hello world")
10: }
11:
=> 12: func main() {
13: go g2()
14:
15: (30 * )
16: println("main exit")
17: }
Looking directly at the main function, we can't see thatgo
keyword does, look at the CPU's assembly instructions:
(dlv) si
> () ./:13 (PC: 0x462395)
:12 0x462384 7645 jbe 0x4623cb
:12 0x462386 55 push rbp
:12 0x462387 4889e5 mov rbp, rsp
:12 0x46238a* 4883ec10 sub rsp, 0x10
:13 0x46238e 488d050b7a0100 lea rax, ptr [rip+0x17a0b]
=> :13 0x462395 e8c6b1fdff call $
:15 0x46239a 48b800505c18a3010000 mov rax, 0x1a3185c5000
:15 0x4623a4 e8b79fffff call $
As can be seen.go
keyword is compiled and converted to actually call the$
function, which is used in theGo runtime scheduler in a nutshell (4): running the main goroutine It has been covered in great detail, so I won't repeat it here.
It is important to note the order in which the main goroutine and the normal goroutine are executed. When calling the After that, the gp is added to P's runnable queue (or to the global queue if the queue is full), and the thread is then scheduled to run the gp. However, for the
newproc
For example, after gp is placed in the queue.newproc
It exits. The subsequent main goroutine code is executed.
If gp is not running or has not finished, and the main goroutine is not waiting/blocking, the main goroutine will just exit.
2. Exit of gp
The difference between gp and the main goroutine is mainly in the exit of the goroutine. main goroutine's exit is a bit more brutal, with a direct call to theexit(0)
Exit the process. So how does gp exit?
We are ing2
Break point at the end point. Look at that.g2
How did it quit:
(dlv) b ./:10
Breakpoint 1 set at 0x46235b for main.g2() ./:10
(dlv) c
hello world
> main.g2() ./:10 (hits goroutine(5):1 total:1) (PC: 0x46235b)
7: func g2() {
8: (10 * )
9: println("hello world")
=> 10: }
11:
12: func main() {
13: go g2()
14:
15: (30 * )
(dlv) si
> main.g2() ./:10 (PC: 0x46235f)
:9 0x462345 488d05b81b0100 lea rax, ptr [rip+0x11bb8]
:9 0x46234c bb0c000000 mov ebx, 0xc
:9 0x462351 e88a30fdff call $
:9 0x462356 e86528fdff call $
:10 0x46235b* 4883c410 add rsp, 0x10
=> :10 0x46235f 5d pop rbp
:10 0x462360 c3 ret
:7 0x462361 e89ab1ffff call $runtime.morestack_noctxt
:7 0x462366 ebb8 jmp $main.g2
The CPU executes instructions topop rbp
The next step is to execute ret.
:10 0x46235f 5d pop rbp
=> :10 0x462360 c3 ret
:7 0x462361 e89ab1ffff call $runtime.morestack_noctxt
:7 0x462366 ebb8 jmp $main.g2
(dlv) si
> () /usr/local/go/src/runtime/asm_amd64.s:1651 (PC: 0x45d7a1)
Warning: debugging optimized function
TEXT (SB) /usr/local/go/src/runtime/asm_amd64.s
asm_amd64.s:1650 0x45d7a0 90 nop
=> asm_amd64.s:1651 0x45d7a1 e8ba250000 call $runtime.goexit1
asm_amd64.s:1653 0x45d7a6 90 nop
What do we see? Executing ret jumps us directly to thecall $runtime.goexit1
. Remember that inGo runtime scheduler in a nutshell (3): main goroutine creation It says that every goroutine stack puts the "top of the stack" in thefuncPC(goexit) + 1
The stack of gp will jump to the address of the ret when it exits the execution of ret. The actual stealing is done here, and gp's stack is jumped to whenever it exits the execution retcall $runtime.goexit1
Continued implementation.
go intoruntime.goexit1
:
// Finishes execution of the current goroutine.
func goexit1() {
...
mcall(goexit0) // mcall will switch the current stack to g0 a wooden or bamboo pen for sheep or cattle,follow g0 a wooden or bamboo pen for sheep or cattle执行 goexit0
}
The actual implementation ofgoexit0
:
// goexit continuation on g0.
func goexit0(gp *g) {
mp := getg().m // Here is the g0 stack, mp = m0
pp := () // P bound by m0.
casgstatus(gp, _Grunning, _Gdead) // update gp's status to _Gdead
= nil // update gp's bound thread to nil, unbinding it from the thread
...
dropg() // Unbind the current thread from gp.
...
gfput(pp, gp) // The exiting gp can still be reused. gfput puts the gp into a local or global free queue.
...
schedule() // If the thread has finished executing a gp and hasn't exited, go back to schedule and look for a goroutine to execute.
}
gp exits, the thread doesn't exit, the thread settles gp down and then proceeds to start a new round of scheduling, it's a real labor of love.
3. Summary
This presentation introduces the use ofgo
How the goroutine created by the keyword works, in the next lecture we relax and look at a few case studies to analyze the behavior of the scheduler.