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

news/2025/1/15 15:07:15/文章来源:https://www.cnblogs.com/xingzheanan/p/18414500

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 0x4623cbgoexit.go:12    0x462386        55                      push rbpgoexit.go:12    0x462387        4889e5                  mov rbp, rspgoexit.go:12    0x46238a*       4883ec10                sub rsp, 0x10goexit.go:13    0x46238e        488d050b7a0100          lea rax, ptr [rip+0x17a0b]
=>      goexit.go:13    0x462395        e8c6b1fdff              call $runtime.newprocgoexit.go:15    0x46239a        48b800505c18a3010000    mov rax, 0x1a3185c5000goexit.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, 0xcgoexit.go:9     0x462351        e88a30fdff      call $runtime.printstringgoexit.go:9     0x462356        e86528fdff      call $runtime.printunlockgoexit.go:10    0x46235b*       4883c410        add rsp, 0x10
=>      goexit.go:10    0x46235f        5d              pop rbpgoexit.go:10    0x462360        c3              retgoexit.go:7     0x462361        e89ab1ffff      call $runtime.morestack_noctxtgoexit.go:7     0x462366        ebb8            jmp $main.g2

CPU 执行指令到 pop rbp,接着执行 ret:

        goexit.go:10    0x46235f        5d              pop rbp
=>      goexit.go:10    0x462360        c3              retgoexit.go:7     0x462361        e89ab1ffff      call $runtime.morestack_noctxtgoexit.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.sasm_amd64.s:1650        0x45d7a0        90              nop
=>      asm_amd64.s:1651        0x45d7a1        e8ba250000      call $runtime.goexit1asm_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 = m0pp := mp.p.ptr()                        // m0 绑定的 Pcasgstatus(gp, _Grunning, _Gdead)       // 将 gp 的状态更新为 _Gdeadgp.m = nil                              // 将 gp 绑定的线程更新为 nil,和线程解绑...dropg()                                 // 将当前线程和 gp 解绑...gfput(pp, gp)                           // 退出的 gp 还是可以重用的,gfput 将 gp 放到本地或者全局空闲队列中...schedule()                              // 线程执行完一个 gp 还没有退出,继续进入 schedule 找 goroutine 执行
}

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

3. 小结

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


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/797142.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

前端项目通过 Nginx 发布至 Linux,并通过 rewrite 配置访问后端接口

本文通过将 arco 框架的前端项目,部署至 CentOS 7,并访问同服务器的 WebAPI 接口,来简单演示一下,如何将前端项目发布至 Linux 系统。〇、前言 本文通过将 arco 框架的前端项目,部署至 CentOS 7,并访问同服务器的 WebAPI 接口,来简单演示一下,如何将前端项目发布至 Lin…

练习第四周8.31

作业: 1、安装burp并实现抓取HTTP站点的数据包(HTTPS站点暂时不要求)2、练习Tomcat PUT方法任意写文件漏洞(CVE-2017-12615),提供蚁剑连接成功截图3、练习S2-048 远程代码执行漏洞(CVE-2017-9791),提供命令执行截图 4、练习JBoss 5.x/6.x 反序列化漏洞(CVE-2017-1214…

【csp201912-2】回收站选址

题目背景开学了,可是校园里堆积了不少垃圾杂物。热心的同学们纷纷自发前来清理,为学校注入正能量~ 题目描述 通过无人机航拍我们已经知晓了n处尚待清理的垃圾位置,其中第i(1≤i≤n)处的坐标为(x,y),保证所有的坐标均为整数。我们希望在垃圾集中的地方建立些回收站。具体来说…

【Ehviewer绿色版】1.9.8.4最新版本下载2024安卓苹果

Ehviewer 是一款主要用于浏览和下载漫画、插画等二次元图像内容的软件。适用安卓和苹果系统,Ehviewer拥有海量的漫画作品,涵盖各种题材和风格,包括日本漫画、韩国漫画、欧美漫画以及国内的一些同人创作等。无论是热门的商业漫画还是小众的独立作品,都能在 Ehviewer上找到,…

uniapp - uView 组件库的u-button 不支持 @click.stop事件,会报错 - 解决

包一层view即可,点击事件不要写在按钮上 本文来自博客园,作者:岑惜,转载请注明原文链接:https://www.cnblogs.com/c2g5201314/p/18414452响应开源精神相互学习,内容良币驱除劣币

章13——常用类——包装类,Integer类

包装类ctrl + b 可以跳转源代码。char 和 boolean的继承体系:包装类和基本数据的转换//装箱int n = 200;Integer integer = n;//拆箱int n1 = integer;包装类练习题三元运算符中是一个整体,其中精度最高的是double,所以无论结果返回什么,都会提高obj1的精度。 包装类到Stri…

大模型应用开发初探 : 手搓一个简易Agent

本文简单介绍AI Agent的基本概念 和 工作方式,目前主要有两种开发Agent的模式,一种是高代码手搓,另一种是低代码拖拉拽。然后,通过C# + Semantic Kernel + 智谱LLM模型 演示了如何快速开发一个简易的AI Agent,虽然它只是个Demo,但希望对你快速了解Agent有所帮助!大家好,…

安装vCenter VCSA 7.0 报错 Failed to run vdcpromo 的问题

百度了一下说是DNS的问题,但我也设置了8.8.8.8或电信的dns,都不行。外网找了一下说要设置为127.0.0.1,但是7.0U3a后的版本不允许填写127.0.0.1了。最后找到一个通过CLI的方式安装可以避免这个问题。 首先创建一个CLI的横版文件,内容如下: {"__version": "2…

PostSync介绍

PostSync 促进技术文章发展介绍 这是一个开源的同步文章的软件,你可以使用它来同步你的文章到多个平台。 使用打开浏览器,登录各个平台的账号,掘金、CSDN、知乎、公众号、哔哩哔哩、博客园、个人WordPress 打开config.yaml文件,配置你的浏览器信息以及浏览器用户数据目录 运…

【linux】centos7安装8.4.2版本mysql

1、前置: 清除mysql相关数据rpm -qa | grep mariadbrpm -e --nodeps 查出来的文件名rpm -qa | grep mysqlrpm -e --nodeps 查出来的文件名2、安装mysql依赖包(没试过不装会有什么问题)# 查找libaio [root@node2 ~]# rpm -qa|grep libaio# 安装libaio [root@node2 ~]# yum -y…