golang goroutine 如何退出?

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

//Themaingoroutine.
funcmain(){
//g=maingoroutine,不再是g0了
g:=getg()//……………………ifsys.PtrSize==8{
maxstacksize=1000000000
}else{
maxstacksize=250000000
}//AllownewproctostartnewMs.
mainStarted=truesystemstack(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没执行完,同样会直接退出。

packagemainimport"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),DIget_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.mzph.cn/news/712545.shtml

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

相关文章

Python列表中添加删除元素不走弯路

1.append() 向列表中添加单个元素,一般用于尾部追加 list1 ["香妃", "乾隆", "贾南风", "赵飞燕", "汉武帝"]list1.append("周瑜") print(list1) # [香妃, 乾隆, 贾南风, 赵飞燕, 汉武帝, 周瑜]…

STM32标准库——(14)I2C通信协议、MPU6050简介

1.I2C通信 I2C 通讯协议(Inter-Integrated Circuit)是由Phiilps公司开发的,由于它引脚少,硬件实现简单,可扩展性强, 不需要USART、CAN等通讯协议的外部收发设备,现在被广泛地使用在系统内多个集成电路(IC)间…

【LeetCode每日一题】【BFS模版与例题】863.二叉树中所有距离为 K 的结点

BFS的基本概念 BFS 是广度优先搜索(Breadth-First Search)的缩写,是一种图遍历算法。它从给定的起始节点开始,逐层遍历图中的节点,直到遍历到目标节点或者遍历完所有可达节点。 BFS 算法的核心思想是先访问当前节点的…

计算机网络_2.2物理层下面的传输媒体

2.2物理层下面的传输媒体 一、传输媒体的分类二、导向型传输媒体1、同轴电缆2、双绞线3、光纤(1)光纤通信原理(2)光纤组成(4)多模光纤与单模光纤对比(5)光纤的波长与规格&#xff08…

海量淘宝商品数据如何实现自动化抓取?

随着电子商务的飞速发展,淘宝作为中国最大的网络购物平台之一,其商品数据具有极高的商业价值。然而,如何有效地从海量的淘宝商品数据中抓取所需信息,成为了一个技术挑战。本文将深入探讨如何实现淘宝商品数据的自动化抓取&#xf…

c# using 用法

using命令空间 导入命名空间中的所有类型 如:using System.Text; using别名 using别名包括详细命名空间信息的具体类型,这种做法有个好处就是当同一个cs引用了两个不同的命名空间,但两个命名空间都包括了一个相同名字的类型的时候。当需要…

SQL加锁机制深度解析:不同隔离级别与索引类型的影响

首先,我们先理解一下涉及的几个核心概念: 主键 (Primary Key): 主键是数据库表中的特殊列,用于唯一标识表中的每一行。它不能有重复值,也不能有NULL值。 唯一索引 (Unique Index): 唯一索引类似于主键,但它允许NULL值…

数据可视化基础与应用-02-基于powerbi实现连锁糕点店数据集的仪表盘制作

总结 本系列是数据可视化基础与应用的第02篇,主要介绍基于powerbi实现一个连锁糕点店数据集的仪表盘制作。 数据集描述 有一个数据集,包含四张工作簿,每个工作簿是一张表,其中可以销售表可以划分为事实表,产品表&am…

【Python小技巧】将list变量写入本地txt文件并读出为list变量的方法(附代码)

文章目录 前言一、万能的txt和eval大法二、具体代码和使用方法总结 前言 使用Python,我们偶尔需要将一些变量保存到本地,并被其它代码读取作为参数,那么怎么办呢? 一、万能的txt和eval大法 这里教大家一个简单的方法&#xff0c…

912. 排序数组(快速排序)

快速排序: 分:找到分成两部分进行排序的pos(使用partition)治:分别对这两部分进行快速排序 重点:partition 找到pivot(两个方法:1. 取第一个值;2. 取随机值&#xff09…

Linux时间同步(PPS、PTP、chrony)分析笔记

1 PPS(pulse per second) 1.1 简介 LinuxPPS provides a programming interface (API) to define in the system several PPS sources. PPS means "pulse per second" and a PPS source is just a device which provides a high precision signal each second so t…

每日一题 2673使二叉树所有路径值相等的最小代价

2673. 使二叉树所有路径值相等的最小代价 题目描述: 给你一个整数 n 表示一棵 满二叉树 里面节点的数目,节点编号从 1 到 n 。根节点编号为 1 ,树中每个非叶子节点 i 都有两个孩子,分别是左孩子 2 * i 和右孩子 2 * i 1 。 树…

Java缓存简介

内存访问速度和硬盘访问速度是计算机系统中两个非常重要的性能指标。 内存访问速度:内存是计算机中最快的存储介质,它的访问速度可以达到几纳秒级别。内存中的数据可以直接被CPU访问,因此读写速度非常快。 硬盘访问速度&…

学习和工作的投入产出比(节选)

人工智能统领全文 推荐包含关于投入、产出、过剩、市场关注、案例、结果和避雷等主题的信息: 投入与产出: 投入和产出都有直接和间接两类常见形式。常见的四种组合是:直接投入、直接产出、间接投入、间接产出。 过剩: 过剩是一个重…

力扣SQL50 无效的推文 查询

Problem: 1683. 无效的推文 思路 👨‍🏫 参考 char_length(str):计算 str 的字符长度length(str):计算 str 的字节长度 Code select tweet_id from Tweets where char_length(content) > 15;

C++与 Fluke5500A设备通过GPIB-USB-B通信的经验积累

C与 Fluke5500A设备通过GPIB-USB-B通信的经验积累 以下内容来自:C与 Fluke5500A设备通过GPIB-USB-B通信的经验积累 - JMarcus - 博客园 (cnblogs.com)START 1.需要安装NI-488.2.281,安装好了之后,GPIB-USB-B的驱动就自动安装好了 注意版本…

动态规划(算法竞赛、蓝桥杯)--单调队列滑动窗口与连续子序列的最大和

1、B站视频链接&#xff1a;E11【模板】单调队列 滑动窗口最值_哔哩哔哩_bilibili 题目链接&#xff1a;滑动窗口 /【模板】单调队列 - 洛谷 #include <bits/stdc.h> using namespace std; const int N1000010; int a[N],q[N];//q存的是元素的下标 int main(){int n,k;…

unity学习(41)——创建(create)角色脚本(panel)——UserHandler(收)+CreateClick(发)——创建发包!

1.客户端的程序结构被我精简过&#xff0c;现在去MessageManager.cs中增加一个UserHandler函数&#xff0c;根据收到的包做对应的GameInfo赋值。 2.在Model文件夹下新增一个协议文件UserProtocol&#xff0c;内容很简单。 using System;public class UserProtocol {public co…

金融短信群发平台具有那些特点

金融短信群发平台的特点主要包括以下几个方面&#xff1a; 1.高效性&#xff1a;金融短信群发平台能够快速地发送大量的短信&#xff0c;使得金融信息能够迅速传达给目标客户&#xff0c;保证了信息的及时性和有效性。 2.安全性&#xff1a;金融短信群发平台对于信息的安全性非…

蓝桥杯练习系统(算法训练)ALGO-995 24点

资源限制 内存限制&#xff1a;256.0MB C/C时间限制&#xff1a;1.0s Java时间限制&#xff1a;3.0s Python时间限制&#xff1a;5.0s 问题描述 24点游戏是一个非常有意思的游戏&#xff0c;很流行&#xff0c;玩法很简单&#xff1a;给你4张牌&#xff0c;每张牌上有数…