Location>code7788 >text

Go runtime scheduler in a nutshell (6): non-main goroutine runs

Popularity:886 ℃/2024-09-19 11:34:46

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 thenewproc 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 rbpThe 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.