golang goroutine 如何退出?

news2024/7/6 18:32:58

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

//Themaingoroutine.
funcmain(){
//g=maingoroutine,不再是g0了
g:=getg()

//……………………

ifsys.PtrSize==8{
maxstacksize=1000000000
}else{
maxstacksize=250000000
}

//AllownewproctostartnewMs.
mainStarted=true

systemstack(func(){
//创建监控线程,该线程独立于调度器,不需要跟p关联即可运行
newm(sysmon,nil)
})

lockOSThread()

ifg.m!=&m0{
throw("runtime.mainnotonm0")
}

//调用runtime包的初始化函数,由编译器实现
runtime_init()//mustbebeforedefer
ifnanotime()==0{
throw("nanotimereturningzero")
}

//Deferunlocksothatruntime.Goexitduringinitdoestheunlocktoo.
needUnlock:=true
deferfunc(){
ifneedUnlock{
unlockOSThread()
}
}()

//Recordwhentheworldstarted.Mustbeafterruntime_init
//becausenanotimeonsomeplatformsdependsonstartNano.
runtimeInitTime=nanotime()

//开启垃圾回收器
gcenable()

main_init_done=make(chanbool)

//……………………

//main包的初始化,递归的调用我们import进来的包的初始化函数
fn:=main_init
fn()
close(main_init_done)

needUnlock=false
unlockOSThread()

//……………………

//调用main.main函数
fn=main_main
fn()
ifraceenabled{
racefini()
}

//……………………

//进入系统调用,退出进程,可以看出maingoroutine并未返回,而是直接进入系统调用退出进程了
exit(0)
//保护性代码,如果exit意外返回,下面的代码会让该进程crash死掉
for{
varx*int32
*x=0
}
}

main函数执行流程如下图:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

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

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

packagemain

import"fmt"

funcmain(){
gofunc(){fmt.Println("helloqcrao.com")}()
}

在这个例子中,maingorutine退出时,还来不及执行go出去的函数,整个进程就直接退出了,打印语句不会执行。因此,maingoroutine不会等待其他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返回的指令。所以,maingoroutine执行完后,直接调用exit(0)退出整个进程。

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

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

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

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

//src/runtime/asm_amd64.s

//Thetop-mostfunctionrunningonagoroutine
//returnstogoexit+PCQuantum.
TEXTruntime·goexit(SB),NOSPLIT,$0-0
BYTE    $0x90    //NOP
CALL    runtime·goexit1(SB)    //doesnotreturn
//tracebackfromgoexit1musthitcoderangeofgoexit
BYTE    $0x90    //NOP

直接调用runtime·goexit1

//src/runtime/proc.go
//Finishesexecutionofthecurrentgoroutine.
funcgoexit1(){
//……………………
mcall(goexit0)
}

调用mcall函数:

//切换到g0栈,执行fn(g)
//Fn不能返回
TEXTruntime·mcall(SB),NOSPLIT,$0-8
//取出参数的值放入DI寄存器,它是funcval对象的指针,此场景中fn.fn是goexit0的地址
MOVQ    fn+0(FP),DI

get_tls(CX)
//AX=g
MOVQ    g(CX),AX   //savestateing->sched
//mcall返回地址放入BX
MOVQ    0(SP),BX   //caller'sPC
//g.sched.pc=BX,保存g的PC
MOVQ    BX,(g_sched+gobuf_pc)(AX)
LEAQ    fn+0(FP),BX    //caller'sSP
//保存g的SP
MOVQ    BX,(g_sched+gobuf_sp)(AX)
MOVQ    AX,(g_sched+gobuf_g)(AX)
MOVQ    BP,(g_sched+gobuf_bp)(AX)

//switchtom->g0&itsstack,callfn
MOVQ    g(CX),BX
MOVQ    g_m(BX),BX
//SI=g0
MOVQ    m_g0(BX),SI
CMPQ    SI,AX  //ifg==m->g0callbadmcall
JNE3(PC)
MOVQ    $runtime·badmcall(SB),AX
JMPAX
//把g0的地址设置到线程本地存储中
MOVQ    SI,g(CX)   //g=m->g0
//从g的栈切换到了g0的栈D
MOVQ    (g_sched+gobuf_sp)(SI),SP  //sp=m->g0->sched.sp
//AX=g,参数入栈
PUSHQ   AX
MOVQ    DI,DX
//DI是结构体funcval实例对象的指针,它的第一个成员才是goexit0的地址
//读取第一个成员到DI寄存器
MOVQ    0(DI),DI
//调用goexit0(g)
CALL    DI
POPQ    AX
MOVQ    $runtime·badmcall2(SB),AX
JMPAX
RET

函数参数是:

typefuncvalstruct{
fnuintptr
//variable-size,fn-specificdatahere
}

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

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

L7将tls保存到CX寄存器,L9将当前线程指向的goroutine(非maingoroutine,称为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:

//goexitcontinuationong0.
//在g0上执行
funcgoexit0(gp*g){
//g0
_g_:=getg()

casgstatus(gp,_Grunning,_Gdead)
ifisSystemGoroutine(gp){
atomic.Xadd(&sched.ngsys,-1)
}

//清空gp的一些字段
gp.m=nil
gp.lockedm=nil
_g_.m.lockedg=nil
gp.paniconfault=false
gp._defer=nil//shouldbetruealreadybutjustincase.
gp._panic=nil//non-nilforGoexitduringpanic.pointsatstack-allocateddata.
gp.writebuf=nil
gp.waitreason=""
gp.param=nil
gp.labels=nil
gp.timer=nil

//Notethatgp'sstackscanisnow"valid"becauseithasno
//stack.
gp.gcscanvalid=true
//解除g与m的关系
dropg()

if_g_.m.locked&^_LockExternal!=0{
print("invalidm->locked=",_g_.m.locked,"\n")
throw("internallockOSThreaderror")
}
_g_.m.locked=0
//将g放入free队列缓存起来
gfput(_g_.m.p.ptr(),gp)
schedule()
}

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

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

1.清空g的一些字段;

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

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

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

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

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

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

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

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

本文节选于Go合集《Go 语言问题集》:GOLANG ROADMAP 一个专注Go语言学习、求职的社区。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

前端学习第三天-css基础

1. CSS简介 从HTML被发明开始,样式就以各种形式存在。不同的浏览器结合它们各自的样式语言为用户提供页面效果的控制。最初的HTML只包含很少的显示属性。 随着HTML的成长,为了满足页面设计者的要求,HTML添加了很多显示功能。但是随着这些功能…

Linux命令行与shell脚本编程大全-2.2

第二部分 shell脚本编程基础 第11章构建基础脚本 第12章结构化命令 第13章更多的结构化命令 第14章处理用户输入 第15章呈现数据 第16章脚本控制 第15章 呈现数据 15.1 理解输入和输出 15.1.1 标准文件描述符 Linux 系统会将每个对象当作文件来处理,这包括输入和…

TCP为什么要三次握手?

TCP三次握手协议是为了在不可靠的互联网环境中可靠地建立起一个连接,三次握手可以确保两端的发送和接收能力都是正常的。 那么,为什么是三次而不是二次或四次握手呢? 为什么不是二次握手? 如果是二次握手,即客户端发…

带着问题阅读源码——Spring MVC是如何将url注册到RequestMappingHandlerMapping?

背景 在 Spring MVC 中,DispatcherServlet 是前端控制器(front controller),它负责接收所有的 HTTP 请求并将它们映射到相应的处理器(handler)。为了实现这一点,Spring MVC 使用了适配器模式将…

设计模式-结构型模式-外观模式

外观模式(Facade),为子系统中的一组接口提供一个一致的界面,此模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。[DP] 首先,定义子系统的各个组件接口和具体实现类: // 子系统组件接…

MySQL 用了哪种默认隔离级别,实现原理是什么?

MySQL 的默认隔离级别是 RR - 可重复读,可以通过命令来查看 MySQL 中的默认隔离级别。 RR - 可重复读是基于多版本并发控制(Multi-Version Concurrency Control,MVCC )实现的。MVCC,在读取数据时通过一种类似快照的方…

if语句用法

if语句是单条件分支语句 定义:根据一个条件来控制程序执行流程(如图3.2)。 语法格式: if(表达式){ 若干语句 } ★注意★: ① 表达式的值必须是boolean 型; ② 不能用0代表false;用1代表 true&am…

qt 5.15版本安装

1.qt5.15版本安装 2.安装慢时,切换到清华镜像源:.\qt-unified-windows-x64-online.exe --mirror https://mirrors.tuna.tsinghua.edu.cn/qt/ 3.没有qt 5.15版本在旁边进行筛选,只选archive

MySql外连接

目录 数据准备外连接 数据准备 -- 部门表 create table tb_dept (id int unsigned primary key auto_increment comment 主键ID,name varchar(10) not null unique comment 部门名称,create_time datetime not null comment 创建时间,update_time datetime…

ABB双语言共享充电宝投资理财源码/共享充电宝系统源码/共享充电宝市场分析/五级分销返利+地图显示模式

ABB双语言共享充电宝投资理财源码/五级分销返利地图显示模式/vue编译后前端 测试环境:Linux系统CentOS7.6、宝塔、PHP7.3、MySQL5.6,根目录public,伪静态laravel5, 源码下载:https://download.csdn.net/download/m0_…

2022年CSP-J认证 CCF信息学奥赛C++ 中小学初级组 第一轮真题-完善程序题解析

2022CCF认证第一轮&#xff08;CSP-J&#xff09;真题 三、完善程序题 第一题 枚举因数 从小到大打印正整数n的所有正因数。试补全枚举程序 #include <iostream> using namespace std;int main(){int n;cin >> n;vector<int> fac;fac.reserve((int)ceil(…

备战蓝桥杯---线段树基础1

引入&#xff1a;RMQ问题&#xff1a; 什么是RMQ&#xff1f; 显然&#xff0c;我们无法用前缀维护&#xff0c;因此&#xff0c;我们需要用到线段树的知识&#xff1a; 什么是线段树&#xff1f; 线段树是用一种树状结构存储一个连续区间信息的数据结构 下面我们用图解释用…

2024全国水科技大会暨高氨氮废水厌氧氨氧化处理技术论坛(四)

一、会议背景 为积极应对“十四五”期间我国生态环境治理面临的挑战&#xff0c;加快生态环境科技创新&#xff0c;构建绿色技术创新体系&#xff0c;全面落实科学技术部、生态环境部等部委编制的《“十四五”生态环境领域科技创新专项规划》&#xff0c;积极落实省校合作&…

物联网与智慧城市的融合:构建智能化、便捷化、绿色化的城市未来

一、引言 随着科技的飞速发展和城市化的不断推进&#xff0c;物联网技术正逐步渗透到城市的各个领域&#xff0c;成为推动智慧城市建设的核心力量。物联网与智慧城市的融合&#xff0c;不仅为城市治理提供了高效、智能的解决方案&#xff0c;也为市民的生活带来了前所未有的便…

Docker Swarm全解析:实现微服务高可用与故障转移的秘密武器

&#x1f407;明明跟你说过&#xff1a;个人主页 &#x1f3c5;个人专栏&#xff1a;《Docker入门到精通》 《k8s入门到实战》&#x1f3c5; &#x1f516;行路有良友&#xff0c;便是天堂&#x1f516; 目录 一、基本概念和介绍 1、Docker Swarm 是什么&#xff0c;它与 …

APP自动化测试-入门示例

入门示例 通过上一篇博客APP自动化测试介绍-CSDN博客的学习&#xff0c;相信大家对APP自动化测试已经有了一定的了解&#xff0c;下面演示一下入门示例 1. 配置Appium 1.1. 点击Appium图标&#xff0c;打开服务器&#xff1a; 1.2. 点击Edit Configurations,进入配置页面&am…

elment-ui table表格排序后 清除排序箭头/恢复默认排序 的高亮样式

问题描述&#xff1a; 1.默认排序是按照名称升序排列&#xff08;图一&#xff09; 2.在选择了筛选项以及其他排序方式之后&#xff0c;箭头高亮是这样的&#xff08;图二&#xff09; 3.当我点击清空按钮后&#xff0c;类型清空了&#xff0c;并且传给后端的排序方式是名称/升…

文本多分类

还在用BERT做文本分类&#xff1f;分享一套基于预训练模型ERNIR3.0的文本多分类全流程实例【文本分类】_ernir 文本分类-CSDN博客 /usr/bin/python3 -m pip install --upgrade pip python3-c"import platform;print(platform.architecture()[0]);print(platform.machine…

StarRocks实战——表设计规范与监控体系

目录 前言 一、StarRocks表设计 1.1 字段类型 1.2 分区分桶 1.2.1 分区规范 1.2.2 分桶规范 1.3 主键表 1.3.1 数据有冷热特征 1.3.2 大宽表 1.4 实际案例 1.4.1 案例一&#xff1a;主键表内存优化 1.4.2 案例一&#xff1a;Update内存超了&#xff0c;导致主键表导…

【AI Agent系列】【MetaGPT多智能体学习】5. 多智能体案例拆解 - 基于MetaGPT的智能体辩论(附完整代码)

本系列文章跟随《MetaGPT多智能体课程》&#xff08;https://github.com/datawhalechina/hugging-multi-agent&#xff09;&#xff0c;深入理解并实践多智能体系统的开发。 本文为该课程的第四章&#xff08;多智能体开发&#xff09;的第三篇笔记。主要是对课程刚开始环境搭…