首页 > 其他分享 >Go runtime 调度器精讲(六):非 main goroutine 运行

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

时间:2024-09-14 18:34:52浏览次数:8  
标签:runtime gp 精讲 goroutine goexit go main

原创文章,欢迎转载,转载请注明出处,谢谢。


0. 前言

Go runtime 调度器精讲(三):main goroutine 创建 介绍了 main goroutine 的创建,文中我们说 main goroutine 和非 main goroutine 有区别。当时卖了个关子并未往下讲,这一讲我们会继续介绍非 main goroutine (也就是 go 关键字创建的 goroutine,后文统称为 gp) 的运行,并且把这个关子解开,说一说它们的区别在哪儿。

1. gp 的创建

首先看一个示例:

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。从 Go runtime 调度器精讲(四):运行 main goroutine 可知 main goroutine 运行完之后就调用 exit(0) 退出了。为了能进入 gp,我们这里在 main goroutine 中加了 1 分钟的等待时间。

Go runtime 的启动在前几讲都有介绍,这里直接进入 main 函数,查看 gp 是如何创建的:

(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: }

直接看 main 函数,我们看不出 go 关键字做了什么,查看 CPU 的汇编指令:

(dlv) si
> main.main() ./goexit.go:13 (PC: 0x462395)
        goexit.go:12    0x462384        7645                    jbe 0x4623cb
        goexit.go:12    0x462386        55                      push rbp
        goexit.go:12    0x462387        4889e5                  mov rbp, rsp
        goexit.go:12    0x46238a*       4883ec10                sub rsp, 0x10
        goexit.go:13    0x46238e        488d050b7a0100          lea rax, ptr [rip+0x17a0b]
=>      goexit.go:13    0x462395        e8c6b1fdff              call $runtime.newproc
        goexit.go:15    0x46239a        48b800505c18a3010000    mov rax, 0x1a3185c5000
        goexit.go:15    0x4623a4        e8b79fffff              call $time.Sleep

可以看到,go 关键字被编译转换后实际调用的是 $runtime.newproc 函数,这个函数在 Go runtime 调度器精讲(四):运行 main goroutine 已经非常详细的介绍过了,这里就不赘述了。

有必要在说明的是,main goroutine 和普通 goroutine 执行的顺序。当调用 runtime.newproc 后,gp 被添加到 P 的可运行队列(如果队列满,被添加到全局队列),接着线程会调度运行该 gp。不过对于 newproc 来说,gp 放入队列后,newproc 就退出了。接着执行后续的 main goroutine 代码。

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

2. gp 的退出

前面说 gp 和 main goroutine 的区别主要体现在 goroutine 的退出这里。main goroutine 的退出比较残暴,直接调用 exit(0) 退出进程。那么,gp 是怎么退出的呢?

我们在 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。还记得在 Go runtime 调度器精讲(三):main goroutine 创建 中说每个 goroutine 栈都会在“栈顶”放 funcPC(goexit) + 1 的地址。这里实际是做了一个偷梁换柱,gp 的栈在退出执行 ret 时都会跳转到 call $runtime.goexit1 继续执行。

进入 runtime.goexit1

// Finishes execution of the current goroutine.
func goexit1() {
	...
	mcall(goexit0)                          // mcall 会切换当前栈到 g0 栈,接着在 g0 栈执行 goexit0
}

实际执行的是 goexit0

// goexit continuation on g0.
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 执行
}

gp 退出了,线程并没有退出,线程将 gp 安顿好之后,继续开始新一轮调度,真是劳模啊。

3. 小结

本讲介绍了用 go 关键字创建的 goroutine 是如何运行的,下一讲我们放松放松,看几个案例分析调度器的行为。


标签:runtime,gp,精讲,goroutine,goexit,go,main
From: https://www.cnblogs.com/xingzheanan/p/18414500

相关文章

  • Go runtime 调度器精讲(五):调度策略
    原创文章,欢迎转载,转载请注明出处,谢谢。0.前言在第四讲我们介绍了maingoroutine是如何运行的。其中针对maingoroutine介绍了调度函数schedule是怎么工作的,对于整个调度器的调度策略并没有介绍,这点是不完整的,这一讲会完善调度器的调度策略部分。1.调度时间点runtim......
  • IMA的binary_runtime_measurement文件格式
    首先贴出一个IMA度量文件的实例:binary_runtime_measurement:00000000000a00009d4c81b9dbf2b4c527177f49|.....L......'..I|00000010759de98fdc50a2f6000600006d692d61|u....P......mi-a|00000020676e00310000001a0000687......
  • Go runtime 调度器精讲(四):运行 main goroutine
    原创文章,欢迎转载,转载请注明出处,谢谢。0.前言皇天不负有心人,终于我们到了运行maingoroutine环节了。让我们走起来,看看一个goroutine到底是怎么运行的。1.运行goroutine稍微回顾下前面的内容,第一讲Go程序初始化,介绍了Go程序是怎么进入到runtime的,随之揭开runti......
  • Go runtime 调度器精讲(三):main goroutine 创建
    原创文章,欢迎转载,转载请注明出处,谢谢。0.前言回顾下上一讲的内容。主线程m0蓄势待发,准备干活。g0为m0提供了执行环境,P和m0绑定,为m0提供活,也就是goroutine。那么问题来了,活呢?哪里有活给m0干?这一讲我们将介绍m0执行的第一个活,也就是maingoroutine。maingou......
  • PbootCMS模板自动清理runtime缓存
    runtime目录的作用runtime 目录位于PbootCMS的安装目录下,主要用于存储系统运行时产生的临时文件和缓存文件。这些文件包括但不限于:缓存文件日志文件临时文件随着时间的推移,runtime 目录中的文件会逐渐增多,占用大量磁盘空间。当文件过多时,会造成系统性能下降,甚至出现一......
  • Goroutines
    Goroutines是Go语言中的核心并发原语。它们是由Go运行时管理的轻量级线程,能够以更高效的方式进行并发操作。基本概念轻量级线程:Goroutines是比操作系统线程更轻量的执行单元。它们的启动和管理开销很小,可以同时运行成千上万的Goroutines。调度:Go运行时会自动......
  • pbootcms模板自动清理runtime缓存
    //自动会话清理脚本publicfunctionclean_session(){check_dir(RUN_PATH.'/archive',true);$data=json_decode(trim(substr(file_get_contents(RUN_PATH.'/archive/session_ticket.php'),15)));if($data->expire_time&&$......
  • VisualStudio 通过配置 DefaultXamlRuntime 属性 让控制台项目里的 XAML 应用上智能提
    本文记录一个VisualStudio黑科技,通过配置DefaultXamlRuntime属性,即可让非WPF或WinUI或MAUI等系列类型的项目也可以拥有XAML的智能提示,智能提示方式和WinUI智能提示行为相同比如说在一个控制台项目里面,我期望从控制台开始,定制自己的UI框架,比如说到现在还没有支持......
  • Go runtime 调度器精讲(二):调度器初始化
    原创文章,欢迎转载,转载请注明出处,谢谢。0.前言上一讲介绍了Go程序初始化的过程,这一讲继续往下看,进入调度器的初始化过程。接着上一讲的执行过程,省略一些不相关的代码,执行到runtime/asm_amd64.s:rt0_go:343L:(dlv)siasm_amd64.s:3430x45431c*8b442418......
  • 《守望先锋2》游戏启动时崩溃弹窗“找不到vcruntime140.dll”该怎么修复错误?守望先锋2
    当启动《守望先锋2》时,游戏崩溃且弹窗提示“找不到vcruntime140.dll”,这实在令人糟心。别担忧,这个错误是能够修复的。可能需要重新获取该文件并正确安装,或者对系统相关设置进行检查调整。具体怎么做呢?本篇将为大家带来的内容,感兴趣的小伙伴们一起来看看吧,希望能够帮助到大家。......