久趣下载站

当前位置: 首页 » 游戏攻略 » Go runtime 调度器精讲(四):非 main goroutine 运行

Go runtime 调度器精讲(四):非 main goroutine 运行

在前几篇文章中,我们介绍了Go语言中main goroutine的创建过程。本文将继续探讨非main goroutine(使用go关键字创建的goroutine)的运行机制,并解开前文中提到的main goroutine和非main goroutine之间的区别。

首先,我们来看一个示例:

func g2() {
    time.Sleep(10 * time.Second)
    println("hello world")
}

func main() {
    go g2()

    time.Sleep(1 * time.Minute)
    println("main exit")
}

从上述示例中可以看出,main函数创建了两个goroutine,一个是main goroutine,另一个是普通的goroutine。在main goroutine中加入了1分钟的等待时间,以便让非main goroutine有机会运行。

接下来,我们来查看非main goroutine是如何创建的:

(dlv) c
> main.main() ./goexit.go:12 (hits goroutine(1):1 total:1) (PC: 0x46238a)
     7: func g2() {
     8:         time.Sleep(10 * time.Second)
     9:         println("hello world")
    10: }
    11:
=>  12: func main() {
    13:         go g2()
    14:
    15:         time.Sleep(30 * time.Minute)
    16:         println("main exit")
    17: }

通过查看CPU的汇编指令,我们发现go关键字实际上被编译转换为了$runtime.newproc函数的调用。这个函数在前几篇文章中已经详细介绍过,这里就不再赘述。

需要说明的是,main goroutine和普通goroutine的执行顺序。当调用runtime.newproc后,非main goroutine会被添加到P的可运行队列(如果队列满,则添加到全局队列),然后线程会调度运行该goroutine。但对于newproc来说,一旦goroutine被放入队列,newproc就会退出,然后继续执行后续的main goroutine代码。

如果此时非main goroutine未运行或未结束,并且main goroutine未等待或阻塞,main goroutine将直接退出。

前面提到main goroutine和非main goroutine的区别主要体现在goroutine的退出方式。main goroutine的退出比较直接,直接调用exit(0)退出进程。那么,非main goroutine是如何退出的呢?

我们在g2函数的结束点处设置断点,观察g2是如何退出的:

(dlv) b ./goexit.go:10
Breakpoint 1 set at 0x46235b for main.g2() ./goexit.go:10
(dlv) c
hello world
> main.g2() ./goexit.go:10 (hits goroutine(5):1 total:1) (PC: 0x46235b)
     7: func g2() {
     8:         time.Sleep(10 * time.Second)
     9:         println("hello world")
=>  10: }
    11:
    12: func main() {
    13:         go g2()
    14:
    15:         time.Sleep(30 * time.Minute)
(dlv) si
> main.g2() ./goexit.go:10 (PC: 0x46235f)
        goexit.go:9     0x462345        488d05b81b0100  lea rax, ptr [rip+0x11bb8]
        goexit.go:9     0x46234c        bb0c000000      mov ebx, 0xc
        goexit.go:9     0x462351        e88a30fdff      call $runtime.printstring
        goexit.go:9     0x462356        e86528fdff      call $runtime.printunlock
        goexit.go:10    0x46235b*       4883c410        add rsp, 0x10
=>      goexit.go:10    0x46235f        5d              pop rbp
        goexit.go:10    0x462360        c3              ret
        goexit.go:7     0x462361        e89ab1ffff      call $runtime.morestack_noctxt
        goexit.go:7     0x462366        ebb8            jmp $main.g2

CPU执行指令到pop rbp,然后执行ret:

        goexit.go:10    0x46235f        5d              pop rbp
=>      goexit.go:10    0x462360        c3              ret
        goexit.go:7     0x462361        e89ab1ffff      call $runtime.morestack_noctxt
        goexit.go:7     0x462366        ebb8            jmp $main.g2
(dlv) si
> runtime.goexit() /usr/local/go/src/runtime/asm_amd64.s:1651 (PC: 0x45d7a1)
Warning: debugging optimized function
TEXT runtime.goexit(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

我们发现,执行ret后直接跳转到了call $runtime.goexit1。在前文中我们提到,每个goroutine的栈在“栈顶”放有funcPC(goexit) + 1的地址。这里实际上进行了一种偷梁换柱的操作,非main goroutine的栈在退出时会跳转到call $runtime.goexit1继续执行。

进入runtime.goexit1:

// 结束当前goroutine的执行。
func goexit1() {
	...
	mcall(goexit0)                          // mcall会切换当前栈到g0栈,然后在g0栈执行goexit0
}

实际执行的是goexit0:

// 在g0上继续goexit。
func goexit0(gp *g) {
    mp := getg().m                          // 这里是g0栈,mp = m0
	pp := mp.p.ptr()                        // m0绑定的P

    casgstatus(gp, _Grunning, _Gdead)       // 将gp的状态更新为_Gdead
    gp.m = nil                              // 将gp绑定的线程更新为nil,解绑线程
    ...

    dropg()                                 // 将当前线程和gp解绑
    ...
    gfput(pp, gp)                           // 退出的gp还可以重用,gfput将gp放到本地或全局空闲队列中

    ...
    schedule()                              // 线程执行完一个gp还未退出,继续进入schedule找goroutine执行
}

非main goroutine退出了,但线程并没有退出,线程会将非main goroutine安顿好后,继续开始新一轮调度。

本文介绍了使用go关键字创建的goroutine是如何运行的,下一篇文章将继续分析调度器的行为。

猜你喜欢
本类排行