<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          動(dòng)圖圖解!怎么讓goroutine跑一半就退出?

          共 5773字,需瀏覽 12分鐘

           ·

          2022-03-19 12:20

          光看標(biāo)題,大家可能不太理解我說的是啥。

          我們平時(shí)創(chuàng)建一個(gè)協(xié)程,跑一段邏輯,代碼大概長(zhǎng)這樣。

          package main import (    "fmt"    "time")func Foo() {    fmt.Println("打印1")    defer fmt.Println("打印2")    fmt.Println("打印3")} func main() {    go  Foo()    fmt.Println("打印4")    time.Sleep(1000*time.Second)} // 這段代碼,正常運(yùn)行會(huì)有下面的結(jié)果打印4打印1打印3打印2

          注意這上面"打印2"是在defer中的,所以會(huì)在函數(shù)結(jié)束前打印。因此后置于"打印3"。

          那么今天的問題是,如何讓Foo()函數(shù)跑一半就結(jié)束,比如說跑到打印2,就退出協(xié)程。輸出如下結(jié)果

          打印4打印1打印2

          也不賣關(guān)子了,我這邊直接說答案。

          在"打印2"后面插入一個(gè)?runtime.Goexit(), 協(xié)程就會(huì)直接結(jié)束。并且結(jié)束前還能執(zhí)行到defer里的打印2。

          package main import (    "fmt"    "runtime"    "time")func Foo() {    fmt.Println("打印1")    defer fmt.Println("打印2")    runtime.Goexit() // 加入這行    fmt.Println("打印3")} func main() {    go  Foo()    fmt.Println("打印4")    time.Sleep(1000*time.Second)}  // 輸出結(jié)果打印4打印1打印2

          可以看到打印3這一行沒出現(xiàn)了,協(xié)程確實(shí)提前結(jié)束了。

          其實(shí)面試題到這里就講完了,這一波自問自答可還行?

          但這不是今天的重點(diǎn),我們需要搞搞清楚內(nèi)部的邏輯。

          runtime.Goexit()是什么?

          看一下內(nèi)部實(shí)現(xiàn)。

          func Goexit() {    // 以下函數(shù)省略一些邏輯...    gp := getg()     for {    // 獲取defer并執(zhí)行        d := gp._defer        reflectcall(nil, unsafe.Pointer(d.fn), deferArgs(d), uint32(d.siz), uint32(d.siz))    }    goexit1()} func goexit1() {    mcall(goexit0)}

          從代碼上看,runtime.Goexit()會(huì)先執(zhí)行一下defer里的方法,這里就解釋了開頭的代碼里為什么在defer里的打印2能正常輸出。

          然后代碼再執(zhí)行goexit1。本質(zhì)就是對(duì)goexit0的簡(jiǎn)單封裝。

          我們可以把代碼繼續(xù)跟下去,看看goexit0做了什么。

          // goexit continuation on g0.func goexit0(gp *g) {  // 獲取當(dāng)前的 goroutine    _g_ := getg()    // 將當(dāng)前goroutine的狀態(tài)置為 _Gdead    casgstatus(gp, _Grunning, _Gdead)  // 全局協(xié)程數(shù)減一    if isSystemGoroutine(gp, false) {        atomic.Xadd(&sched.ngsys, -1)    }    // 省略各種清空邏輯...   // 把g從m上摘下來。  dropg()      // 把這個(gè)g放回到p的本地協(xié)程隊(duì)列里,放不下放全局協(xié)程隊(duì)列。    gfput(_g_.m.p.ptr(), gp)   // 重新調(diào)度,拿下一個(gè)可運(yùn)行的協(xié)程出來跑    schedule()} 

          這段代碼,信息密度比較大。

          很多名詞可能讓人一臉懵。

          簡(jiǎn)單描述下,Go語言里有個(gè)GMP模型的說法,M是內(nèi)核線程,G也就是我們平時(shí)用的協(xié)程goroutine,P會(huì)在G和M之間做工具人,負(fù)責(zé)調(diào)度GM上運(yùn)行。

          GMP圖

          既然是調(diào)度,也就是說不是每個(gè)G都能一直處于運(yùn)行狀態(tài),等G不能運(yùn)行時(shí),就把它存起來,再調(diào)度下一個(gè)能運(yùn)行的G過來運(yùn)行。

          暫時(shí)不能運(yùn)行的G,P上會(huì)有個(gè)本地隊(duì)列去存放這些這些G,P的本地隊(duì)列存不下的話,還有個(gè)全局隊(duì)列,干的事情也類似。

          了解這個(gè)背景后,再回到?goexit0?方法看看,做的事情就是將當(dāng)前的協(xié)程G置為_Gdead狀態(tài),然后把它從M上摘下來,嘗試放回到P的本地隊(duì)列中。然后重新調(diào)度一波,獲取另一個(gè)能跑的G,拿出來跑。

          goexit

          所以簡(jiǎn)單總結(jié)一下,只要執(zhí)行 goexit 這個(gè)函數(shù),當(dāng)前協(xié)程就會(huì)退出,同時(shí)還能調(diào)度下一個(gè)可執(zhí)行的協(xié)程出來跑。

          看到這里,大家應(yīng)該就能理解,開頭的代碼里,為什么runtime.Goexit()能讓協(xié)程只執(zhí)行一半就結(jié)束了。

          goexit的用途

          看是看懂了,但是會(huì)忍不住疑惑。面試這么問問,那只能說明你遇到了一個(gè)喜歡為難年輕人的面試官,但正經(jīng)人誰會(huì)沒事跑一半?yún)f(xié)程就結(jié)束呢?所以goexit真實(shí)用途是啥?

          有個(gè)小細(xì)節(jié),不知道大家平時(shí)debug的時(shí)候有沒有關(guān)注過。

          為了說明問題,這里先給出一段代碼。

          package main import (    "fmt"    "time")func Foo() {    fmt.Println("打印1")} func main() {    go  Foo()    fmt.Println("打印3")    time.Sleep(1000*time.Second)}

          這是一段非常簡(jiǎn)單的代碼,輸出什么完全不重要。通過go關(guān)鍵字啟動(dòng)了一個(gè)goroutine執(zhí)行Foo(),里面打印一下就結(jié)束,主協(xié)程sleep很長(zhǎng)時(shí)間,只為死等。

          這里我們新啟動(dòng)的協(xié)程里,在Foo()函數(shù)內(nèi)隨便打個(gè)斷點(diǎn)。然后debug一下。

          會(huì)發(fā)現(xiàn),這個(gè)協(xié)程的堆棧底部是從runtime.goexit()里開始啟動(dòng)的。

          如果大家平時(shí)有注意觀察,會(huì)發(fā)現(xiàn),其實(shí)所有的堆棧底部,都是從這個(gè)函數(shù)開始的。我們繼續(xù)跟跟代碼。

          goexit是什么?

          從上面的debug堆棧里點(diǎn)進(jìn)去會(huì)發(fā)現(xiàn),這是個(gè)匯編函數(shù),可以看出調(diào)用的是runtime包內(nèi)的?goexit1()?函數(shù)。

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

          于是跟到了pruntime/proc.go里的代碼中。

          // 省略部分代碼func goexit1() {    mcall(goexit0)}

          是不是很熟悉,這不就是我們開頭講runtime.Goexit()里內(nèi)部執(zhí)行的goexit0嗎。

          為什么每個(gè)堆棧底部都是這個(gè)方法?

          我們首先需要知道的是,函數(shù)棧的執(zhí)行過程,是先進(jìn)后出。

          假設(shè)我們有以下代碼

          func main() {    B()} func B() {    A()} func A() { }

          上面的代碼是main運(yùn)行B函數(shù),B函數(shù)再運(yùn)行A函數(shù),代碼執(zhí)行時(shí)就跟下面的動(dòng)圖那樣。

          函數(shù)堆棧執(zhí)行順序

          這個(gè)是先進(jìn)后出的過程,也就是我們常說的函數(shù)棧,執(zhí)行完子函數(shù)A()后,就會(huì)回到父函數(shù)B()中,執(zhí)行完B()后,最后就會(huì)回到main()。這里的棧底是main(),如果在棧底插入的是?goexit?的話,那么當(dāng)程序執(zhí)行結(jié)束的時(shí)候就都能跑到goexit里去。

          結(jié)合前面講過的內(nèi)容,我們就能知道,此時(shí)棧底的goexit,會(huì)在協(xié)程內(nèi)的業(yè)務(wù)代碼跑完后被執(zhí)行到,從而實(shí)現(xiàn)協(xié)程退出,并調(diào)度下一個(gè)可執(zhí)行的G來運(yùn)行。


          那么問題又來了,棧底插入goexit這件事是誰做的,什么時(shí)候做的?

          直接說答案,這個(gè)在runtime/proc.go里有個(gè)newproc1方法,只要是創(chuàng)建協(xié)程都會(huì)用到這個(gè)方法。里面有個(gè)地方是這么寫的。

          func newproc1(fn *funcval, argp unsafe.Pointer, narg int32, callergp *g, callerpc uintptr) {    // 獲取當(dāng)前g  _g_ := getg()    // 獲取當(dāng)前g所在的p    _p_ := _g_.m.p.ptr()  // 創(chuàng)建一個(gè)新 goroutine    newg := gfget(_p_)     // 底部插入goexit    newg.sched.pc = funcPC(goexit) + sys.PCQuantum     newg.sched.g = guintptr(unsafe.Pointer(newg))    // 把新創(chuàng)建的g放到p中    runqput(_p_, newg, true)     // ...} 

          主要的邏輯是獲取當(dāng)前協(xié)程G所在的調(diào)度器P,然后創(chuàng)建一個(gè)新G,并在棧底插入一個(gè)goexit。

          所以我們每次debug的時(shí)候,就都能看到函數(shù)棧底部有個(gè)goexit函數(shù)。

          main函數(shù)也是個(gè)協(xié)程,棧底也是goexit?

          關(guān)于main函數(shù)棧底是不是也有個(gè)goexit,我們對(duì)下面代碼斷點(diǎn)看下。直接得出結(jié)果。

          main函數(shù)棧底也是goexit()。

          從?asm_amd64.s可以看到Go程序啟動(dòng)的流程,這里提到的?runtime·mainPC?其實(shí)就是?runtime.main.

              // create a new goroutine to start program    MOVQ    $runtime·mainPC(SB), AX        // 也就是runtime.main    PUSHQ    AX    PUSHQ    $0            // arg size    CALL    runtime·newproc(SB)

          通過runtime·newproc創(chuàng)建runtime.main協(xié)程,然后在runtime.main里會(huì)啟動(dòng)main.main函數(shù),這個(gè)就是我們平時(shí)寫的那個(gè)main函數(shù)了。

          // runtime/proc.gofunc main() {    // 省略大量代碼    fn := main_main // 其實(shí)就是我們的main函數(shù)入口    fn() } //go:linkname main_main main.mainfunc main_main()

          結(jié)論是,其實(shí)main函數(shù)也是由newproc創(chuàng)建的,只要通過newproc創(chuàng)建的goroutine,棧底就會(huì)有一個(gè)goexit。

          os.Exit()和runtime.Goexit()有什么區(qū)別

          最后再回到開頭的問題,實(shí)現(xiàn)一下首尾呼應(yīng)。

          開頭的面試題,除了runtime.Goexit(),是不是還可以改為用os.Exit()

          同樣都是帶有"退出"的含義,兩者退出的對(duì)象不同。os.Exit()?指的是整個(gè)進(jìn)程退出;而runtime.Goexit()指的是協(xié)程退出。

          可想而知,改用os.Exit()?這種情況下,defer里的內(nèi)容就不會(huì)被執(zhí)行到了。

          package main import (    "fmt"    "os"    "time")func Foo() {    fmt.Println("打印1")    defer fmt.Println("打印2")    os.Exit(0)    fmt.Println("打印3")} func main() {    go  Foo()    fmt.Println("打印4")    time.Sleep(1000*time.Second)} // 輸出結(jié)果打印4打印1 

          總結(jié)

          ?通過?runtime.Goexit()可以做到提前結(jié)束協(xié)程,且結(jié)束前還能執(zhí)行到defer的內(nèi)容??runtime.Goexit()其實(shí)是對(duì)goexit0的封裝,只要執(zhí)行 goexit0 這個(gè)函數(shù),當(dāng)前協(xié)程就會(huì)退出,同時(shí)還能調(diào)度下一個(gè)可執(zhí)行的協(xié)程出來跑。?通過newproc可以創(chuàng)建出新的goroutine,它會(huì)在函數(shù)棧底部插入一個(gè)goexit。?os.Exit()?指的是整個(gè)進(jìn)程退出;而runtime.Goexit()指的是協(xié)程退出。兩者含義有區(qū)別。

          最后

          無用的知識(shí)又增加了。

          一般情況下,業(yè)務(wù)開發(fā)中,誰會(huì)沒事執(zhí)行這個(gè)函數(shù)呢?

          但是開發(fā)中不關(guān)心,不代表面試官不關(guān)心!

          下次面試官問你,如果想在goroutine執(zhí)行一半就退出協(xié)程,該怎么辦?你知道該怎么回答了吧?


          好了,兄弟們,有沒有發(fā)現(xiàn)這篇文章寫的又水又短,真的是因?yàn)槲易儜辛藛幔?/p>

          不!

          當(dāng)然不!

          我是為了兄弟們的身體健康考慮,保持蹲姿太久對(duì)身體不好,懂?


          如果文章對(duì)你有幫助,歡迎.....

          算了。

          一起在知識(shí)的海洋里嗆水吧

          我是小白,我們下期見!


          點(diǎn)擊下方名片,關(guān)注公眾號(hào):【小白debug】

          不滿足于在留言區(qū)說騷話?

          加我,我們建了個(gè)劃水吹牛皮群,在群里,你可以跟你下次跳槽可能遇到的同事或面試官聊點(diǎn)有趣的話題。就超!開!心!

          文章推薦:

          ?程序員防猝死指南?TCP粘包 數(shù)據(jù)包:我只是犯了每個(gè)數(shù)據(jù)包都會(huì)犯的錯(cuò) |硬核圖解?動(dòng)圖圖解!既然IP層會(huì)分片,為什么TCP層也還要分段?

          參考資料

          饒大的《哪來里的 goexit?》- https://qcrao.com/2021/06/07/where-is-goexit-from/


          瀏覽 39
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  美女无码做爱 | 韩国av三级片麻豆 | 爱搞搞就爱搞 | 又黄又爽无遮挡 | 午夜亚洲一区 |