首页 星云 工具 资源 星选 资讯 热门工具
:

PDF转图片 完全免费 小红书视频下载 无水印 抖音视频下载 无水印 数字星空

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

编程知识
2024年09月14日 17:40

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


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 是如何运行的,下一讲我们放松放松,看几个案例分析调度器的行为。


From:https://www.cnblogs.com/xingzheanan/p/18414500
本文地址: http://www.shuzixingkong.net/article/2030
0评论
提交 加载更多评论
其他文章 前端项目通过 Nginx 发布至 Linux,并通过 rewrite 配置访问后端接口
本文通过将 arco 框架的前端项目,部署至 CentOS 7,并访问同服务器的 WebAPI 接口,来简单演示一下,如何将前端项目发布至 Linux 系统。
前端项目通过 Nginx 发布至 Linux,并通过 rewrite 配置访问后端接口 前端项目通过 Nginx 发布至 Linux,并通过 rewrite 配置访问后端接口
记一次 公司.NET项目部署在Linux环境压测时 内存暴涨分析
一:背景 讲故事 公司部署在某碟上的项目在9月份压测50并发时,发现某个容器线程、内存非正常的上涨,导致功能出现了异常无法使用。根据所学,自己分析了下线程和内存问题,分析时可以使用lldb或者windbg,但是个人比较倾向于界面化的windbg,所以最终使用windbg开干。 二:WinDbg 分析
擅长处理临时数据的结构——栈
目录实践1 —— 从字符串中移除星号 栈和数组存储数据的方式一样,它们都只是元素的列表。不同之处在于栈的以下3个限制: 数据只能从栈末插入; 数据只能从栈末删除; 只能读取栈的最后一个元素。 栈和队列、链表...一样,都是抽象的数据结构, 何为抽象数据结构? 它指一种数据组织的形式,它不关注具体的实
擅长处理临时数据的结构——栈 擅长处理临时数据的结构——栈 擅长处理临时数据的结构——栈
Codes 开源研发项目管理平台——创新的敏捷测试解决方案
Codes 是国内首款重新定义 SaaS 模式的开源项目管理平台,支持云端认证、本地部署、全部功能开放,并且对 30 人以下团队免费。它通过整合迭代、看板、度量和自动化等功能,简化测试协同工作,使敏捷测试更易于实施。并提供低成本的敏捷测试解决方案,如同步在线离线测试用例、流程化管理缺陷、低代码接口自
Codes 开源研发项目管理平台——创新的敏捷测试解决方案 Codes 开源研发项目管理平台——创新的敏捷测试解决方案 Codes 开源研发项目管理平台——创新的敏捷测试解决方案
分析负数取模与取余的规则
目录负数"取模"基本概念修正定义取整规则决定商的值取模和取余不一样. 负数"取模" 基本概念 如果a和d是两个自然数,d非零,可以证明存在两个唯一的整数 q 和 r,满足 a = q*d + r,且0 <= r < d。其中,q 被称为商,r 被称
分析负数取模与取余的规则 分析负数取模与取余的规则 分析负数取模与取余的规则
CMake构建学习笔记16-使用VS进行CMake项目的开发
详细介绍了通过Visual Studio 2019 这款IDE进行CMake项目开发过程,能够极大增加C/C++程序的开发效率。
CMake构建学习笔记16-使用VS进行CMake项目的开发 CMake构建学习笔记16-使用VS进行CMake项目的开发 CMake构建学习笔记16-使用VS进行CMake项目的开发
痞子衡嵌入式:JLink命令行以及JFlash对于下载算法的作用地址范围认定
大家好,我是痞子衡,是正经搞技术的痞子。今天痞子衡给大家分享的是JLink命令行以及JFlash对于下载算法的作用地址范围认定。 最近痞子衡在给一个 RT1170 客户定制一个 Infineon MirrorBit 类型 64MB Flash 的 SEGGER 下载算法,做完之后在 JFlash 下
痞子衡嵌入式:JLink命令行以及JFlash对于下载算法的作用地址范围认定 痞子衡嵌入式:JLink命令行以及JFlash对于下载算法的作用地址范围认定 痞子衡嵌入式:JLink命令行以及JFlash对于下载算法的作用地址范围认定
全网最适合入门的面向对象编程教程:49 Python函数方法与接口-函数与方法的区别和lamda匿名函数
在 Python 中,函数和方法都是代码的基本单元,用于封装和执行特定的任务。它们之间有一些重要的区别,而 lambda 匿名函数则是 Python 提供的一种简洁定义小型函数的方法。
全网最适合入门的面向对象编程教程:49 Python函数方法与接口-函数与方法的区别和lamda匿名函数 全网最适合入门的面向对象编程教程:49 Python函数方法与接口-函数与方法的区别和lamda匿名函数 全网最适合入门的面向对象编程教程:49 Python函数方法与接口-函数与方法的区别和lamda匿名函数