文章目录

  • GoLang之goroutine如何退出(11)

GoLang之goroutine如何退出(11)

上一讲说到调度器将 main goroutine 推上舞台,为它铺好了道路,开始执行 runtime.main 函数。这一讲,我们探索 main goroutine 以及普通 goroutine 从执行到退出的整个过程。

// The main goroutine.
func main() {// g = main goroutine,不再是 g0 了g := getg()// ……………………if sys.PtrSize == 8 {maxstacksize = 1000000000} else {maxstacksize = 250000000}// Allow newproc to start new Ms.mainStarted = truesystemstack(func() {// 创建监控线程,该线程独立于调度器,不需要跟 p 关联即可运行newm(sysmon, nil)})lockOSThread()if g.m != &m0 {throw("runtime.main not on m0")}// 调用 runtime 包的初始化函数,由编译器实现runtime_init() // must be before deferif nanotime() == 0 {throw("nanotime returning zero")}// Defer unlock so that runtime.Goexit during init does the unlock too.needUnlock := truedefer func() {if needUnlock {unlockOSThread()}}()// Record when the world started. Must be after runtime_init// because nanotime on some platforms depends on startNano.runtimeInitTime = nanotime()// 开启垃圾回收器gcenable()main_init_done = make(chan bool)// ……………………// main 包的初始化,递归的调用我们 import 进来的包的初始化函数fn := main_initfn()close(main_init_done)needUnlock = falseunlockOSThread()// ……………………// 调用 main.main 函数fn = main_mainfn()if raceenabled {racefini()}// ……………………// 进入系统调用,退出进程,可以看出 main goroutine 并未返回,而是直接进入系统调用退出进程了exit(0)// 保护性代码,如果 exit 意外返回,下面的代码会让该进程 crash 死掉for {var x *int32*x = 0}
}

main 函数执行流程如下图:

从流程图可知,main goroutine 执行完之后就直接调用 exit(0) 退出了,这会导致整个进程退出,太粗暴了。

不过,main goroutine 实际上就是代表用户的 main 函数,它都执行完了,肯定是用户的任务都执行完了,直接退出就可以了,就算有其他的 goroutine 没执行完,同样会直接退出。

package mainimport "fmt"func main() {go func() {fmt.Println("hello qcrao.com")}()
}

在这个例子中,main goroutine 退出时,还来不及执行 go 出去 的函数,整个进程就直接退出了,打印语句不会执行。因此,main goroutine 不会等待其他 goroutine 执行完再退出,知道这个有时能解释一些现象,比如上面那个例子。

这时,心中可能会跳出疑问,我们在新创建 goroutine 的时候,不是整出了个“偷天换日”,风风火火地设置了 goroutine 退出时应该跳到 runtime.goexit 函数吗,怎么这会不用了,闲得慌?

回顾一下上一讲的内容,跳转到 main 函数的两行代码:

// 把 sched.pc 值放入 BX 寄存器
MOVQ    gobuf_pc(BX), BX
// JMP 把 BX 寄存器的包含的地址值放入 CPU 的 IP 寄存器,于是,CPU 跳转到该地址继续执行指令
JMP BX

直接使用了一个跳转,并没有使用 CALL 指令,而 runtime.main 函数中确实也没有 RET 返回的指令。所以,main goroutine 执行完后,直接调用 exit(0) 退出整个进程。

那之前整地“偷天换日”还有用吗?有的!这是针对非 main goroutine 起作用。

参考资料【阿波张 非 goroutine 的退出】中用调试工具验证了非 main goroutine 的退出,感兴趣的可以去跟着实践一遍。

我们继续探索非 main goroutine (后文我们就称 gp 好了)的退出流程。

gp 执行完后,RET 指令弹出 goexit 函数地址(实际上是 funcPC(goexit)+1),CPU 跳转到 goexit 的第二条指令继续执行:

// src/runtime/asm_amd64.s// The top-most function running on a goroutine
// returns to goexit+PCQuantum.
TEXT runtime·goexit(SB),NOSPLIT,$0-0BYTE    $0x90   // NOPCALL  runtime·goexit1(SB) // does not return// traceback from goexit1 must hit code range of goexitBYTE   $0x90   // NOP

直接调用 runtime·goexit1

// src/runtime/proc.go
// Finishes execution of the current goroutine.
func goexit1() {// …………………… mcall(goexit0)
}

调用 mcall 函数:

// 切换到 g0 栈,执行 fn(g)
// Fn 不能返回
TEXT runtime·mcall(SB), NOSPLIT, $0-8// 取出参数的值放入 DI 寄存器,它是 funcval 对象的指针,此场景中 fn.fn 是 goexit0 的地址MOVQ    fn+0(FP), DIget_tls(CX)// AX = gMOVQ    g(CX), AX   // save state in g->sched// mcall 返回地址放入 BXMOVQ    0(SP), BX   // caller's PC// g.sched.pc = BX,保存 g 的 PCMOVQ    BX, (g_sched+gobuf_pc)(AX)LEAQ    fn+0(FP), BX    // caller's SP// 保存 g 的 SPMOVQ    BX, (g_sched+gobuf_sp)(AX)MOVQ    AX, (g_sched+gobuf_g)(AX)MOVQ    BP, (g_sched+gobuf_bp)(AX)// switch to m->g0 & its stack, call fnMOVQ    g(CX), BXMOVQ    g_m(BX), BX// SI = g0MOVQ    m_g0(BX), SICMPQ    SI, AX  // if g == m->g0 call badmcallJNE 3(PC)MOVQ    $runtime·badmcall(SB), AXJMP AX// 把 g0 的地址设置到线程本地存储中MOVQ    SI, g(CX)   // g = m->g0// 从 g 的栈切换到了 g0 的栈DMOVQ    (g_sched+gobuf_sp)(SI), SP  // sp = m->g0->sched.sp// AX = g,参数入栈PUSHQ   AXMOVQ    DI, DX// DI 是结构体 funcval 实例对象的指针,它的第一个成员才是 goexit0 的地址// 读取第一个成员到 DI 寄存器MOVQ    0(DI), DI// 调用 goexit0(g)CALL    DIPOPQ    AXMOVQ    $runtime·badmcall2(SB), AXJMP AXRET

函数参数是:

type funcval struct {fn uintptr// variable-size, fn-specific data here
}

字段 fn 就表示 goexit0 函数的地址。

L5 将函数参数保存到 DI 寄存器,这里 fn.fn 就是 goexit0 的地址。

L7 将 tls 保存到 CX 寄存器,L9 将 当前线程指向的 goroutine (非 main goroutine,称为 gp)保存到 AX 寄存器,L11 将调用者(调用 mcall 函数)的栈顶,这里就是 mcall 完成后的返回地址,存入 BX 寄存器。

L13 将 mcall 的返回地址保存到 gp 的 g.sched.pc 字段,L14 将 gp 的栈顶,也就是 SP 保存到 BX 寄存器,L16 将 SP 保存到 gp 的 g.sched.sp 字段,L17 将 g 保存到 gp 的 g.sched.g 字段,L18 将 BP 保存 到 gp 的 g.sched.bp 字段。这一段主要是保存 gp 的调度信息。

L21 将当前指向的 g 保存到 BX 寄存器,L22 将 g.m 字段保存到 BX 寄存器,L23 将 g.m.g0 字段保存到 SI,g.m.g0 就是当前工作线程的 g0。

现在,SI = g0, AX = gp,L25 判断 gp 是否是 g0,如果 gp == g0 说明有问题,执行 runtime·badmcall。正常情况下,PC 值加 3,跳过下面的两条指令,直接到达 L30。

L30 将 g0 的地址设置到线程本地存储中,L32 将 g0.SP 设置到 CPU 的 SP 寄存器,这也就意味着我们从 gp 栈切换到了 g0 的栈,要变天了!

L34 将参数 gp 入栈,为调用 goexit0 构造参数。L35 将 DI 寄存器的内容设置到 DX 寄存器,DI 是结构体 funcval 实例对象的指针,它的第一个成员才是 goexit0 的地址。L36 读取 DI 第一成员,也就是 goexit0 函数的地址。

L40 调用 goexit0 函数,这已经是在 g0 栈上执行了,函数参数就是 gp。

到这里,就会去执行 goexit0 函数,注意,这里永远都不会返回。所以,在 CALL 指令后面,如果返回了,又会去调用 runtime.badmcall2 函数去处理意外情况。

来继续看 goexit0:

// goexit continuation on g0.
// 在 g0 上执行
func goexit0(gp *g) {// g0_g_ := getg()casgstatus(gp, _Grunning, _Gdead)if isSystemGoroutine(gp) {atomic.Xadd(&sched.ngsys, -1)}// 清空 gp 的一些字段gp.m = nilgp.lockedm = nil_g_.m.lockedg = nilgp.paniconfault = falsegp._defer = nil // should be true already but just in case.gp._panic = nil // non-nil for Goexit during panic. points at stack-allocated data.gp.writebuf = nilgp.waitreason = ""gp.param = nilgp.labels = nilgp.timer = nil// Note that gp's stack scan is now "valid" because it has no// stack.gp.gcscanvalid = true// 解除 g 与 m 的关系dropg()if _g_.m.locked&^_LockExternal != 0 {print("invalid m->locked = ", _g_.m.locked, "\n")throw("internal lockOSThread error")}_g_.m.locked = 0// 将 g 放入 free 队列缓存起来gfput(_g_.m.p.ptr(), gp)schedule()
}

它主要完成最后的清理工作:

1.把 g 的状态从 _Grunning 更新为 _Gdead

2.清空 g 的一些字段;

3.调用 dropg 函数解除 g 和 m 之间的关系,其实就是设置 g->m = nil, m->currg = nil;

4.把 g 放入 p 的 freeg 队列缓存起来供下次创建 g 时快速获取而不用从内存分配。freeg 就是 g 的一个对象池;

5.调用 schedule 函数再次进行调度。

到这里,gp 就完成了它的历史使命,功成身退,进入了 goroutine 缓存池,待下次有任务再重新启用。

而工作线程,又继续调用 schedule 函数进行新一轮的调度,整个过程形成了一个循环。

总结一下,main goroutine 和普通 goroutine 的退出过程:

对于 main goroutine,在执行完用户定义的 main 函数的所有代码后,直接调用 exit(0) 退出整个进程,非常霸道。

对于普通 goroutine 则没这么“舒服”,需要经历一系列的过程。先是跳转到提前设置好的 goexit 函数的第二条指令,然后调用 runtime.goexit1,接着调用 mcall(goexit0),而 mcall 函数会切换到 g0 栈,运行 goexit0 函数,清理 goroutine 的一些字段,并将其添加到 goroutine 缓存池里,然后进入 schedule 调度循环。到这里,普通 goroutine 才算完成使命。

参考资料

【阿波张 非 main goroutine 的退出及调度循环】

GoLang之goroutine如何退出(11)相关推荐

  1. golang--监控goroutine异常退出

    在golang中,我们可以很轻易产生数以万计的goroutine,不过这也带来了麻烦:在运行中某一个goroutine异常退出,怎么办? 在erlang中,有link原语,2个进程可以链接在一起,一个 ...

  2. 一看就懂系列之Golang的goroutine和通道

    https://blog.csdn.net/u011957758/article/details/81159481 前言 如果说php是最好的语言,那么golang就是最并发的语言. 支持golang ...

  3. golang的goroutine调度模型

    golang的goroutine调度模型 进程:一个在内存中运行的程序 线程:进程中的一个控制单元,一个进程至少由一个线程,也可以由多个线程,主要由CPU进行调度. 形象理解:进程就是一个生产某样产品 ...

  4. go语言之行--golang核武器goroutine调度原理、channel详解

    一.goroutine简介 goroutine是go语言中最为NB的设计,也是其魅力所在,goroutine的本质是协程,是实现并行计算的核心.goroutine使用方式非常的简单,只需使用go关键字 ...

  5. [转]Golang中goroutine的调度器详解

    Go调度器原理浅析 来源:https://www.douban.com/note/300631999/ goroutine是golang的一大特色,或者可以说是最大的特色吧(据我了解),这篇文章主要翻 ...

  6. Golang信号处理和优雅退出守护进程

    Golang中的信号处理 信号类型 个平台的信号定义或许有些不同.下面列出了POSIX中定义的信号. Linux 使用34-64信号用作实时系统中. 命令 man signal 提供了官方的信号介绍. ...

  7. Golang笔记——goroutine(协程)

    goroutine-看一个需求 需求:要求统计 1-9000000000 的数字中,哪些是素数? 分析思路: 传统的方法,就是使用一个循环,循环的判断各个数是不是素数.[很慢] 使用并发或者并行的方式 ...

  8. Golang中Goroutine与线程

    我们在使用Go语言进行开发时,一般会使用goroutine来处理并发任务.那么大家有没有考虑过goroutine的实现机制是什么样的?很多同学会把goroutine与线程等同起来,但是实际上并不是这样 ...

  9. golang的goroutine调度机制

    一直对goroutine的调度机制很好奇,最近在看雨痕的golang源码分析,(基于go1.4) 感觉豁然开朗,受益匪浅: 去繁就简,再加上自己的一些理解,整理了一下 ~~ 调度器 主要基于三个基本对 ...

最新文章

  1. BZOJ3515 : EvenPaths
  2. 1.11 为什么使用卷积-深度学习第四课《卷积神经网络》-Stanford吴恩达教授
  3. python分组函数_Python中如何按列分组和按自己的函数汇总
  4. access开发精要(15)-货币与数字类型格式(3)
  5. 性能测试篇 :Jmeter监控服务器性能
  6. crontab里shell脚本将top信息写入文件
  7. SharePoint2010 连接配置数据库字符串保存位置(转)
  8. 《数据资产管理实践白皮书(5.0版)》正式发布,附下载链接
  9. 杭电4510为什么时光不能倒流
  10. Playing with OS(操作系统)
  11. Tomcat 7 的七大新特性
  12. PMP新考纲启用,如何判断是敏捷?第6版教材与第7版有什么区别?
  13. FIBOS社区发起人 响马:一个“极客硬核老炮儿”是怎样的?
  14. 文件包含漏洞(原理及介绍)
  15. 360视频:金字塔棱台投影TSP
  16. SyntaxError: (unicode error) ‘utf-8‘ codec can‘t decode byte 0xca in position 0: invalid continuati
  17. 实验四-哈夫曼编码的MATLAB实现
  18. 成都Java程序员培训毕业后什么水平
  19. 澳大利亚将建12个光伏发电站
  20. Swagelok SS-62TF4-31C-GC

热门文章

  1. 基于SQL企业采购管理系统设计-VB
  2. 关于计算机知识的活动策划,大学计算机协会活动策划书
  3. iview mysql_iView 发布后台管理系统 iview-admin,没错,它就是你想要的
  4. 6月5日世界环境日:Agoda发布可持续旅游趋势调查报告
  5. 软件测试工程师前景怎样?这3大好处没人能拒绝!
  6. Ubuntu远程桌面连接
  7. JAVA爬取虎嗅网截图_httpclient3+jsoup多线程抓取虎嗅网全部文章
  8. #HTML的块元素和行内元素#
  9. 【Linux学习笔记】Linux Centos7.4下的Ftp服务的搭建和使用及加密
  10. android 高仿点餐,仿饿了吗点餐界面ListView联动的实现