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.pswp.cn/news/712545.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/712545.shtml
英文地址,請注明出處:http://en.pswp.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;每張牌上有數…