Golang GMP

前言

隨著服務(wù)器硬件迭代升級(jí),配置也越來(lái)越高。為充分利用服務(wù)器資源及汉,并發(fā)編程也變的越來(lái)越重要。在開(kāi)始之前屯烦,需要了解一下并發(fā)(concurrency)和并行(parallesim)的區(qū)別坷随。

并發(fā): 邏輯上具有處理多個(gè)同時(shí)性任務(wù)的能力。

并行: 物理上同一時(shí)刻執(zhí)行多個(gè)并發(fā)任務(wù)驻龟。

通常所說(shuō)的并發(fā)編程温眉,也就是說(shuō)它允許多個(gè)任務(wù)同時(shí)執(zhí)行,但實(shí)際上并不一定在同一時(shí)刻被執(zhí)行翁狐。在單核處理器上类溢,通過(guò)多線程共享CPU時(shí)間片串行執(zhí)行(并發(fā)非并行)。而并行則依賴于多核處理器等物理資源露懒,讓多個(gè)任務(wù)可以實(shí)現(xiàn)并行執(zhí)行(并發(fā)且并行)闯冷。
多線程或多進(jìn)程是并行的基本條件,但單線程也可以用協(xié)程(coroutine)做到并發(fā)懈词。簡(jiǎn)單將Goroutine歸納為協(xié)程并不合適蛇耀,因?yàn)樗\(yùn)行時(shí)會(huì)創(chuàng)建多個(gè)線程來(lái)執(zhí)行并發(fā)任務(wù),且任務(wù)單元可被調(diào)度到其它線程執(zhí)行坎弯。這更像是多線程和協(xié)程的結(jié)合體纺涤,能最大限度提升執(zhí)行效率译暂,發(fā)揮多核處理器能力。
Go編寫一個(gè)并發(fā)編程程序很簡(jiǎn)單洒琢,只需要在函數(shù)之前使用一個(gè)Go關(guān)鍵字就可以實(shí)現(xiàn)并發(fā)編程秧秉。

      func main(){
         go func(){
              fmt.Println("Hello, World!")
         }()
   }

Go語(yǔ)言雖然使用go關(guān)鍵字,即實(shí)現(xiàn)并發(fā)編程衰抑,丹Goroutine被調(diào)度到后端之后象迎,具體的實(shí)現(xiàn)比較復(fù)雜。
操作系統(tǒng)中的程序可以往下分解為進(jìn)程呛踊、線程和協(xié)程砾淌。Go中,協(xié)程被稱為goroutine谭网,被runtime進(jìn)行調(diào)度汪厨。
GMP調(diào)度中,G代表goroutine愉择,M代表線程劫乱,P是Processor,它包含了運(yùn)行g(shù)oroutine的資源锥涕,只有當(dāng) M 關(guān)聯(lián)一個(gè) P 后才能執(zhí)行 G 衷戈。

1、G

G是goroutine的縮寫层坠,相當(dāng)于操作系統(tǒng)中的進(jìn)程控制塊殖妇,在這里就是Goroutine的控制結(jié)構(gòu),對(duì)Goroutine的抽象破花,其中包含執(zhí)行函數(shù)指令及參數(shù)谦趣;G保存的任務(wù)對(duì)象;線程上下文切換座每,現(xiàn)場(chǎng)保護(hù)和現(xiàn)場(chǎng)恢復(fù)需要的寄存器(SP,IP)等信息
Go不同版本Goroutine默認(rèn)棧大小不同

     // Go1.11版本默認(rèn)stack大小為2KB

_StackMin = 2048
 
// 創(chuàng)建一個(gè)g對(duì)象,然后放到g隊(duì)列
// 等待被執(zhí)行
func newproc1(fn *funcval, argp *uint8, narg int32, callergp *g, callerpc uintptr) {
    _g_ := getg()

    _g_.m.locks++
    siz := narg
    siz = (siz + 7) &^ 7

    _p_ := _g_.m.p.ptr()
    newg := gfget(_p_)    
    if newg == nil {        
       // 初始化g stack大小
        newg = malg(_StackMin)
        casgstatus(newg, _Gidle, _Gdead)
        allgadd(newg)
    }    
    // 以下省略}

2前鹅、M

M是一個(gè)線程或稱為Machine,所有M是有線程棧的尺栖。如果不對(duì)該線程棧提供內(nèi)存的話嫡纠,系統(tǒng)會(huì)給該線程棧提供內(nèi)存(不同操作系統(tǒng)提供的線程棧大小不同)。當(dāng)指定了線程棧延赌,則M.stack→G.stack,M的PC寄存器指向G提供的函數(shù)叉橱,然后去執(zhí)行挫以。

     type m struct {    
    /*
        1.  所有調(diào)用棧的Goroutine,這是一個(gè)比較特殊的Goroutine。
        2.  普通的Goroutine棧是在Heap分配的可增長(zhǎng)的stack,而g0的stack是M對(duì)應(yīng)的線程棧窃祝。
        3.  所有調(diào)度相關(guān)代碼,會(huì)先切換到該Goroutine的棧再執(zhí)行掐松。
    */
    g0       *g
    curg     *g         // M當(dāng)前綁定的結(jié)構(gòu)體G

    // SP、PC寄存器用于現(xiàn)場(chǎng)保護(hù)和現(xiàn)場(chǎng)恢復(fù)
    vdsoSP uintptr
    vdsoPC uintptr

    // 省略…}

3、P

P(Processor)是一個(gè)抽象的概念大磺,并不是真正的物理CPU抡句。所以當(dāng)P有任務(wù)時(shí)需要?jiǎng)?chuàng)建或者喚醒一個(gè)系統(tǒng)線程來(lái)執(zhí)行它隊(duì)列里的任務(wù)。所以P/M需要進(jìn)行綁定杠愧,構(gòu)成一個(gè)執(zhí)行單元待榔。

P決定了同時(shí)可以并發(fā)任務(wù)的數(shù)量,可通過(guò)GOMAXPROCS限制同時(shí)執(zhí)行用戶級(jí)任務(wù)的操作系統(tǒng)線程流济∪衤啵可以通過(guò)runtime.GOMAXPROCS進(jìn)行指定。在Go1.5之后GOMAXPROCS被默認(rèn)設(shè)置可用的核數(shù)绳瘟,而之前則默認(rèn)為1雕憔。

     // 自定義設(shè)置GOMAXPROCS數(shù)量
func GOMAXPROCS(n int) int {    
    /*
        1.  GOMAXPROCS設(shè)置可執(zhí)行的CPU的最大數(shù)量,同時(shí)返回之前的設(shè)置。
        2.  如果n < 1,則不更改當(dāng)前的值糖声。
    */
    ret := int(gomaxprocs)

    stopTheWorld("GOMAXPROCS")    
    // startTheWorld啟動(dòng)時(shí),使用newprocs斤彼。
    newprocs = int32(n)
    startTheWorld()    
    return ret
}
// 默認(rèn)P被綁定到所有CPU核上
// P == cpu.cores

func getproccount() int32 {    
    const maxCPUs = 64 * 1024
    var buf [maxCPUs / 8]byte


    // 獲取CPU Core
    r := sched_getaffinity(0, unsafe.Sizeof(buf), &buf[0])

    n := int32(0)    
    for _, v := range buf[:r] {        
       for v != 0 {
            n += int32(v & 1)
            v >>= 1
        }
    }    
    if n == 0 {
       n = 1
    }    
    return n
}
// 一個(gè)進(jìn)程默認(rèn)被綁定在所有CPU核上,返回所有CPU core。
// 獲取進(jìn)程的CPU親和性掩碼系統(tǒng)調(diào)用
// rax 204                          ; 系統(tǒng)調(diào)用碼
// system_call sys_sched_getaffinity; 系統(tǒng)調(diào)用名稱
// rid  pid                         ; 進(jìn)程號(hào)
// rsi unsigned int len             
// rdx unsigned long *user_mask_ptr
sys_linux_amd64.s:
TEXT runtime·sched_getaffinity(SB),NOSPLIT,$0
    MOVQ    pid+0(FP), DI
    MOVQ    len+8(FP), SI
    MOVQ    buf+16(FP), DX
    MOVL    $SYS_sched_getaffinity, AX
    SYSCALL
    MOVL    AX, ret+24(FP)
    RET

Go調(diào)度器調(diào)度過(guò)程

首先創(chuàng)建一個(gè)G對(duì)象蘸泻,G對(duì)象保存到P本地隊(duì)列或者是全局隊(duì)列琉苇。P此時(shí)去喚醒一個(gè)M。P繼續(xù)執(zhí)行它的執(zhí)行序蟋恬。M尋找是否有空閑的P翁潘,如果有則將該G對(duì)象移動(dòng)到它本身。接下來(lái)M執(zhí)行一個(gè)調(diào)度循環(huán)(調(diào)用G對(duì)象->執(zhí)行->清理線程→繼續(xù)找新的Goroutine執(zhí)行)歼争。

M執(zhí)行過(guò)程中拜马,隨時(shí)會(huì)發(fā)生上下文切換。當(dāng)發(fā)生上線文切換時(shí)沐绒,需要對(duì)執(zhí)行現(xiàn)場(chǎng)進(jìn)行保護(hù)俩莽,以便下次被調(diào)度執(zhí)行時(shí)進(jìn)行現(xiàn)場(chǎng)恢復(fù)。Go調(diào)度器M的棧保存在G對(duì)象上乔遮,只需要將M所需要的寄存器(SP扮超、PC等)保存到G對(duì)象上就可以實(shí)現(xiàn)現(xiàn)場(chǎng)保護(hù)。當(dāng)這些寄存器數(shù)據(jù)被保護(hù)起來(lái)蹋肮,就隨時(shí)可以做上下文切換了出刷,在中斷之前把現(xiàn)場(chǎng)保存起來(lái)。如果此時(shí)G任務(wù)還沒(méi)有執(zhí)行完坯辩,M可以將任務(wù)重新丟到P的任務(wù)隊(duì)列馁龟,等待下一次被調(diào)度執(zhí)行。當(dāng)再次被調(diào)度執(zhí)行時(shí)漆魔,M通過(guò)訪問(wèn)G的vdsoSP坷檩、vdsoPC寄存器進(jìn)行現(xiàn)場(chǎng)恢復(fù)(從上次中斷位置繼續(xù)執(zhí)行)却音。

image.gif

1、P 隊(duì)列
通過(guò)上圖可以發(fā)現(xiàn)矢炼,P有兩種隊(duì)列:本地隊(duì)列和全局隊(duì)列系瓢。

  • 本地隊(duì)列: 當(dāng)前P的隊(duì)列,本地隊(duì)列是Lock-Free句灌,沒(méi)有數(shù)據(jù)競(jìng)爭(zhēng)問(wèn)題夷陋,無(wú)需加鎖處理,可以提升處理速度涯塔。

  • 全局隊(duì)列:全局隊(duì)列為了保證多個(gè)P之間任務(wù)的平衡肌稻。所有M共享P全局隊(duì)列,為保證數(shù)據(jù)競(jìng)爭(zhēng)問(wèn)題匕荸,需要加鎖處理爹谭。相比本地隊(duì)列處理速度要低于全局隊(duì)列。

2榛搔、上線文切換

簡(jiǎn)單理解為當(dāng)時(shí)的環(huán)境即可诺凡,環(huán)境可以包括當(dāng)時(shí)程序狀態(tài)以及變量狀態(tài)。例如線程切換的時(shí)候在內(nèi)核會(huì)發(fā)生上下文切換践惑,這里的上下文就包括了當(dāng)時(shí)寄存器的值腹泌,把寄存器的值保存起來(lái),等下次該線程又得到cpu時(shí)間的時(shí)候再恢復(fù)寄存器的值尔觉,這樣線程才能正確運(yùn)行凉袱。

對(duì)于代碼中某個(gè)值說(shuō),上下文是指這個(gè)值所在的局部(全局)作用域?qū)ο笳焱O鄬?duì)于進(jìn)程而言专甩,上下文就是進(jìn)程執(zhí)行時(shí)的環(huán)境,具體來(lái)說(shuō)就是各個(gè)變量和數(shù)據(jù)钉稍,包括所有的寄存器變量涤躲、進(jìn)程打開(kāi)的文件、內(nèi)存(堆棧)信息等贡未。

3种樱、線程清理
Goroutine被調(diào)度執(zhí)行必須保證P/M進(jìn)行綁定,所以線程清理只需要將P釋放就可以實(shí)現(xiàn)線程的清理俊卤。什么時(shí)候P會(huì)釋放嫩挤,保證其它G可以被執(zhí)行。P被釋放主要有兩種情況消恍。

  • 主動(dòng)釋放:最典型的例子是俐镐,當(dāng)執(zhí)行G任務(wù)時(shí)有系統(tǒng)調(diào)用,當(dāng)發(fā)生系統(tǒng)調(diào)用時(shí)M會(huì)處于Block狀態(tài)哺哼。調(diào)度器會(huì)設(shè)置一個(gè)超時(shí)時(shí)間佩抹,當(dāng)超時(shí)時(shí)會(huì)將P釋放。

  • 被動(dòng)釋放:如果發(fā)生系統(tǒng)調(diào)用取董,有一個(gè)專門監(jiān)控程序棍苹,進(jìn)行掃描當(dāng)前處于阻塞的P/M組合。當(dāng)超過(guò)系統(tǒng)程序設(shè)置的超時(shí)時(shí)間茵汰,會(huì)自動(dòng)將P資源搶走枢里。去執(zhí)行隊(duì)列的其它G任務(wù)。

終于要來(lái)說(shuō)說(shuō)Golang中最吸引人的goroutine了蹂午,這也是Golang能夠橫空出世的主要原因栏豺。不同于Python基于進(jìn)程的并發(fā)模型,以及C++豆胸、Java等基于線程的并發(fā)模型奥洼。Golang采用輕量級(jí)的goroutine來(lái)實(shí)現(xiàn)并發(fā),可以大大減少CPU的切換⊥砗現(xiàn)在已經(jīng)有太多的文章來(lái)介紹goroutine的用法灵奖,在這里,我們從源碼的角度來(lái)看看其內(nèi)部實(shí)現(xiàn)估盘。

重申一下重點(diǎn):goroutine中的三個(gè)實(shí)體

goroutine中最主要的是三個(gè)實(shí)體為GMP瓷患,其中:

G: 代表一個(gè)goroutine對(duì)象,每次go調(diào)用的時(shí)候遣妥,都會(huì)創(chuàng)建一個(gè)G對(duì)象擅编,它包括棧、指令指針以及對(duì)于調(diào)用goroutines很重要的其它信息箫踩,比如阻塞它的任何channel爱态,其主要數(shù)據(jù)結(jié)構(gòu):

     type g struct {
  stack       stack   // 描述了真實(shí)的棧內(nèi)存,包括上下界

  m              *m     // 當(dāng)前的m
  sched          gobuf   // goroutine切換時(shí)班套,用于保存g的上下文      
  param          unsafe.Pointer // 用于傳遞參數(shù)肢藐,睡眠時(shí)其他goroutine可以設(shè)置param,喚醒時(shí)該goroutine可以獲取
  atomicstatus   uint32
  stackLock      uint32 
  goid           int64  // goroutine的ID
  waitsince      int64 // g被阻塞的大體時(shí)間
  lockedm        *m     // G被鎖定只在這個(gè)m上運(yùn)行
}

其中最主要的當(dāng)然是sched了吱韭,保存了goroutine的上下文吆豹。goroutine切換的時(shí)候不同于線程有OS來(lái)負(fù)責(zé)這部分?jǐn)?shù)據(jù),而是由一個(gè)gobuf對(duì)象來(lái)保存理盆,這樣能夠更加輕量級(jí)痘煤,再來(lái)看看gobuf的結(jié)構(gòu):

     type gobuf struct {
    sp   uintptr
    pc   uintptr
    g    guintptr
    ctxt unsafe.Pointer
    ret  sys.Uintreg
    lr   uintptr
    bp   uintptr // for GOEXPERIMENT=framepointer
}

其實(shí)就是保存了當(dāng)前的棧指針,計(jì)數(shù)器猿规,當(dāng)然還有g(shù)自身衷快,這里記錄自身g的指針是為了能快速的訪問(wèn)到goroutine中的信息。

M:代表一個(gè)線程姨俩,每次創(chuàng)建一個(gè)M的時(shí)候蘸拔,都會(huì)有一個(gè)底層線程創(chuàng)建师郑;所有的G任務(wù),最終還是在M上執(zhí)行调窍,其主要數(shù)據(jù)結(jié)構(gòu):

     type m struct {
    g0      *g     // 帶有調(diào)度棧的goroutine

    gsignal       *g         // 處理信號(hào)的goroutine
    tls           [6]uintptr // thread-local storage
    mstartfn      func()
    curg          *g       // 當(dāng)前運(yùn)行的goroutine
    caughtsig     guintptr 
    p             puintptr // 關(guān)聯(lián)p和執(zhí)行的go代碼
    nextp         puintptr
    id            int32
    mallocing     int32 // 狀態(tài)

    spinning      bool // m是否out of work
    blocked       bool // m是否被阻塞
    inwb          bool // m是否在執(zhí)行寫屏蔽

    printlock     int8
    incgo         bool // m在執(zhí)行cgo嗎
    fastrand      uint32
    ncgocall      uint64      // cgo調(diào)用的總數(shù)
    ncgo          int32       // 當(dāng)前cgo調(diào)用的數(shù)目
    park          note
    alllink       *m // 用于鏈接allm
    schedlink     muintptr
    mcache        *mcache // 當(dāng)前m的內(nèi)存緩存
    lockedg       *g // 鎖定g在當(dāng)前m上執(zhí)行宝冕,而不會(huì)切換到其他m
    createstack   [32]uintptr // thread創(chuàng)建的棧
}

結(jié)構(gòu)體M中有兩個(gè)G是需要關(guān)注一下的,一個(gè)是curg邓萨,代表結(jié)構(gòu)體M當(dāng)前綁定的結(jié)構(gòu)體G地梨。另一個(gè)是g0,是帶有調(diào)度棧的goroutine缔恳,這是一個(gè)比較特殊的goroutine宝剖。普通的goroutine的棧是在堆上分配的可增長(zhǎng)的棧,而g0的棧是M對(duì)應(yīng)的線程的棧歉甚。所有調(diào)度相關(guān)的代碼万细,會(huì)先切換到該goroutine的棧中再執(zhí)行。也就是說(shuō)線程的棧也是用的g實(shí)現(xiàn)铃芦,而不是使用的OS的雅镊。

P:代表一個(gè)處理器,每一個(gè)運(yùn)行的M都必須綁定一個(gè)P刃滓,就像線程必須在么一個(gè)CPU核上執(zhí)行一樣仁烹,由P來(lái)調(diào)度G在M上的運(yùn)行,P的個(gè)數(shù)就是GOMAXPROCS(最大256)咧虎,啟動(dòng)時(shí)固定的卓缰,一般不修改;M的個(gè)數(shù)和P的個(gè)數(shù)不一定一樣多(會(huì)有休眠的M或者不需要太多的M)(最大10000)砰诵;每一個(gè)P保存著本地G任務(wù)隊(duì)列征唬,也有一個(gè)全局G任務(wù)隊(duì)列。P的數(shù)據(jù)結(jié)構(gòu):

     type p struct {
    lock mutex

    id          int32
    status      uint32 // 狀態(tài)茁彭,可以為pidle/prunning/...
    link        puintptr
    schedtick   uint32     // 每調(diào)度一次加1
    syscalltick uint32     // 每一次系統(tǒng)調(diào)用加1
    sysmontick  sysmontick 
    m           muintptr   // 回鏈到關(guān)聯(lián)的m
    mcache      *mcache
    racectx     uintptr

    goidcache    uint64 // goroutine的ID的緩存
    goidcacheend uint64

    // 可運(yùn)行的goroutine的隊(duì)列
    runqhead uint32
    runqtail uint32
    runq     [256]guintptr

    runnext guintptr // 下一個(gè)運(yùn)行的g

    sudogcache []*sudog
    sudogbuf   [128]*sudog

    palloc persistentAlloc // per-P to avoid mutex

    pad [sys.CacheLineSize]byte

其中P的狀態(tài)有Pidle, Prunning, Psyscall, Pgcstop, Pdead总寒;在其內(nèi)部隊(duì)列runqhead里面有可運(yùn)行的goroutine,P優(yōu)先從內(nèi)部獲取執(zhí)行的g理肺,這樣能夠提高效率摄闸。

除此之外,還有一個(gè)數(shù)據(jù)結(jié)構(gòu)需要在這里提及妹萨,就是schedt年枕,可以看做是一個(gè)全局的調(diào)度者:

      type schedt struct {
   goidgen  uint64
    lastpoll uint64

    lock mutex

    midle        muintptr // idle狀態(tài)的m
    nmidle       int32    // idle狀態(tài)的m個(gè)數(shù)
    nmidlelocked int32    // lockde狀態(tài)的m個(gè)數(shù)
    mcount       int32    // 創(chuàng)建的m的總數(shù)
    maxmcount    int32    // m允許的最大個(gè)數(shù)

    ngsys uint32 // 系統(tǒng)中g(shù)oroutine的數(shù)目,會(huì)自動(dòng)更新

    pidle      puintptr // idle的p
    npidle     uint32
    nmspinning uint32 

    // 全局的可運(yùn)行的g隊(duì)列
    runqhead guintptr
    runqtail guintptr
    runqsize int32

    // dead的G的全局緩存
    gflock       mutex
    gfreeStack   *g
    gfreeNoStack *g
    ngfree       int32

    // sudog的緩存中心
    sudoglock  mutex
    sudogcache *sudog
}

大多數(shù)需要的信息都已放在了結(jié)構(gòu)體M乎完、G和P中熏兄,schedt結(jié)構(gòu)體只是一個(gè)殼。可以看到摩桶,其中有M的idle隊(duì)列桥状,P的idle隊(duì)列,以及一個(gè)全局的就緒的G隊(duì)列典格。schedt結(jié)構(gòu)體中的Lock是非常必須的岛宦,如果M或P等做一些非局部的操作,它們一般需要先鎖住調(diào)度器耍缴。

goroutine的運(yùn)行過(guò)程

所有的goroutine都是由函數(shù)newproc來(lái)創(chuàng)建的,但是由于該函數(shù)不能調(diào)用分段棧挽霉,最后真正調(diào)用的是newproc1防嗡。在newproc1中主要進(jìn)行如下動(dòng)作:

     func newproc1(fn *funcval, argp *uint8, narg int32, nret int32, callerpc uintptr) *g {
    newg = malg(_StackMin)
    casgstatus(newg, _Gidle, _Gdead)
    allgadd(newg) 
    newg.sched.sp = sp
    newg.stktopsp = sp
    newg.sched.pc = funcPC(goexit) + sys.PCQuantum 
    newg.sched.g = guintptr(unsafe.Pointer(newg))
    gostartcallfn(&newg.sched, fn)
    newg.gopc = callerpc
    newg.startpc = fn.fn
    ......
}

分配一個(gè)g的結(jié)構(gòu)體
初始化這個(gè)結(jié)構(gòu)體的一些域
將g掛在就緒隊(duì)列
綁定g到一個(gè)m上

這個(gè)綁定只要m沒(méi)有突破上限GOMAXPROCS,就拿一個(gè)m綁定一個(gè)g。如果m的waiting隊(duì)列中有就從隊(duì)列中拿,否則就要新建一個(gè)m,調(diào)用newm侠坎。

     func newm(fn func(), _p_ *p) {
    mp := allocm(_p_, fn)
    mp.nextp.set(_p_)
    mp.sigmask = initSigmask
    execLock.rlock()
    newosproc(mp, unsafe.Pointer(mp.g0.stack.hi))
    execLock.runlock()
}

該函數(shù)其實(shí)就是創(chuàng)建一個(gè)m蚁趁,跟newproc有些相似,之前也說(shuō)了m在底層就是一個(gè)線程的創(chuàng)建实胸,也即是newosproc函數(shù)他嫡,在往下挖可以看到會(huì)根據(jù)不同的OS來(lái)執(zhí)行不同的bsdthread_create函數(shù),而底層就是調(diào)用的runtime.clone:

clone(cloneFlags,stk,unsafe.Pointer(mp),unsafe.Pointer(mp.g0),unsafe.Pointer(funcPC(mstart)))
m創(chuàng)建好之后庐完,線程的入口是mstart钢属,最后調(diào)用的即是mstart1:

func mstart1() {
    _g_ := getg()
    gosave(&_g_.m.g0.sched)
    _g_.m.g0.sched.pc = ^uintptr(0)
    asminit()
    minit()

    if _g_.m == &m0 {
        initsig(false)
    }

    if fn := _g_.m.mstartfn; fn != nil {
        fn()
    }
    schedule()
}

里面最重要的就是schedule了,在schedule中的動(dòng)作大體就是找到一個(gè)等待運(yùn)行的g门躯,然后然后搬到m上淆党,設(shè)置其狀態(tài)為Grunning,直接切換到g的上下文環(huán)境,恢復(fù)g的執(zhí)行。

func schedule() {
    _g_ := getg()

    if _g_.m.lockedg != nil {
        stoplockedm()
        execute(_g_.m.lockedg, false) // Never returns.
    }
}

schedule的執(zhí)行可以大體總結(jié)為:

schedule函數(shù)獲取g => [必要時(shí)休眠] => [喚醒后繼續(xù)獲取] => execute函數(shù)執(zhí)行g(shù) => 執(zhí)行后返回到goexit => 重新執(zhí)行schedule函數(shù)

簡(jiǎn)單來(lái)說(shuō)g所經(jīng)歷的幾個(gè)主要的過(guò)程就是:Gwaiting->Grunnable->Grunning讶凉。經(jīng)歷了創(chuàng)建,到掛在就緒隊(duì)列,到從就緒隊(duì)列拿出并運(yùn)行整個(gè)過(guò)程染乌。

casgstatus(gp, _Gwaiting, _Grunnable)
casgstatus(gp, _Grunnable, _Grunning)
引入了struct M這層抽象。m就是這里的worker,但不是線程懂讯。處理系統(tǒng)調(diào)用中的m不會(huì)占用mcpu數(shù)量,只有干事的m才會(huì)對(duì)應(yīng)到線程.當(dāng)mcpu數(shù)量少于GOMAXPROCS時(shí)可以一直開(kāi)新的線程干活.而goroutine的執(zhí)行則是在m和g都滿足之后通過(guò)schedule切換上下文進(jìn)入的.

搶占式調(diào)度

當(dāng)有很多goroutine需要執(zhí)行的時(shí)候荷憋,是怎么調(diào)度的了,上面說(shuō)的P還沒(méi)有出場(chǎng)呢褐望,在runtime.main中會(huì)創(chuàng)建一個(gè)額外m運(yùn)行sysmon函數(shù)勒庄,搶占就是在sysmon中實(shí)現(xiàn)的。

sysmon會(huì)進(jìn)入一個(gè)無(wú)限循環(huán), 第一輪回休眠20us, 之后每次休眠時(shí)間倍增, 最終每一輪都會(huì)休眠10ms. sysmon中有netpool(獲取fd事件), retake(搶占), forcegc(按時(shí)間強(qiáng)制執(zhí)行g(shù)c), scavenge heap(釋放自由列表中多余的項(xiàng)減少內(nèi)存占用)等處理.

func sysmon() {
    lasttrace := int64(0)
    idle := 0 // how many cycles in succession we had not wokeup somebody
    delay := uint32(0)
    for {
        if idle == 0 { // start with 20us sleep...
            delay = 20
        } else if idle > 50 { // start doubling the sleep after 1ms...
            delay *= 2
        }
        if delay > 10*1000 { // up to 10ms
            delay = 10 * 1000
        }
        usleep(delay)

        ......
    }       
}

里面的函數(shù)retake負(fù)責(zé)搶占:

func retake(now int64) uint32 {
    n := 0
    for i := int32(0); i < gomaxprocs; i++ {
        _p_ := allp[i]
        if _p_ == nil {
            continue
        }
        pd := &_p_.sysmontick
        s := _p_.status
        if s == _Psyscall {
            // 如果p的syscall時(shí)間超過(guò)一個(gè)sysmon tick則搶占該p
            t := int64(_p_.syscalltick)
            if int64(pd.syscalltick) != t {
                pd.syscalltick = uint32(t)
                pd.syscallwhen = now
                continue
            }
            if runqempty(_p_) && atomic.Load(&sched.nmspinning)+atomic.Load(&sched.npidle) > 0 && pd.syscallwhen+10*1000*1000 > now {
                continue
            }
            incidlelocked(-1)
            if atomic.Cas(&_p_.status, s, _Pidle) {
                if trace.enabled {
                    traceGoSysBlock(_p_)
                    traceProcStop(_p_)
                }
                n++
                _p_.syscalltick++
                handoffp(_p_)
            }
            incidlelocked(1)
        } else if s == _Prunning {
            // 如果G運(yùn)行時(shí)間過(guò)長(zhǎng)譬挚,則搶占該G
            t := int64(_p_.schedtick)
            if int64(pd.schedtick) != t {
                pd.schedtick = uint32(t)
                pd.schedwhen = now
                continue
            }
            if pd.schedwhen+forcePreemptNS > now {
                continue
            }
            preemptone(_p_)
        }
    }
    return uint32(n)
}

枚舉所有的P 如果P在系統(tǒng)調(diào)用中(_Psyscall), 且經(jīng)過(guò)了一次sysmon循環(huán)(20us~10ms), 則搶占這個(gè)P锅铅, 調(diào)用handoffp解除M和P之間的關(guān)聯(lián), 如果P在運(yùn)行中(_Prunning), 且經(jīng)過(guò)了一次sysmon循環(huán)并且G運(yùn)行時(shí)間超過(guò)forcePreemptNS(10ms), 則搶占這個(gè)P

并設(shè)置g.preempt = true减宣,g.stackguard0 = stackPreempt盐须。

為什么設(shè)置了stackguard就可以實(shí)現(xiàn)搶占?

因?yàn)檫@個(gè)值用于檢查當(dāng)前棧空間是否足夠, go函數(shù)的開(kāi)頭會(huì)比對(duì)這個(gè)值判斷是否需要擴(kuò)張棧漆腌。

newstack函數(shù)判斷g.stackguard0等于stackPreempt, 就知道這是搶占觸發(fā)的, 這時(shí)會(huì)再檢查一遍是否要搶占贼邓。

搶占機(jī)制保證了不會(huì)有一個(gè)G長(zhǎng)時(shí)間的運(yùn)行導(dǎo)致其他G無(wú)法運(yùn)行的情況發(fā)生阶冈。

總結(jié)
相比大多數(shù)并行設(shè)計(jì)模型,Go比較優(yōu)勢(shì)的設(shè)計(jì)就是P上下文這個(gè)概念的出現(xiàn)塑径,如果只有G和M的對(duì)應(yīng)關(guān)系女坑,那么當(dāng)G阻塞在IO上的時(shí)候,M是沒(méi)有實(shí)際在工作的统舀,這樣造成了資源的浪費(fèi)匆骗,沒(méi)有了P,那么所有G的列表都放在全局誉简,這樣導(dǎo)致臨界區(qū)太大碉就,對(duì)多核調(diào)度造成極大影響。

而goroutine在使用上面的特點(diǎn)闷串,感覺(jué)既可以用來(lái)做密集的多核計(jì)算瓮钥,又可以做高并發(fā)的IO應(yīng)用,做IO應(yīng)用的時(shí)候烹吵,寫起來(lái)感覺(jué)和對(duì)程序員最友好的同步阻塞一樣碉熄,而實(shí)際上由于runtime的調(diào)度,底層是以同步非阻塞的方式在運(yùn)行(即IO多路復(fù)用)肋拔。

所以說(shuō)保護(hù)現(xiàn)場(chǎng)的搶占式調(diào)度和G被阻塞后傳遞給其他m調(diào)用的核心思想锈津,使得goroutine的產(chǎn)生。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末只损,一起剝皮案震驚了整個(gè)濱河市一姿,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌跃惫,老刑警劉巖叮叹,帶你破解...
    沈念sama閱讀 211,948評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異爆存,居然都是意外死亡蛉顽,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,371評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門先较,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)携冤,“玉大人,你說(shuō)我怎么就攤上這事闲勺≡兀” “怎么了?”我有些...
    開(kāi)封第一講書人閱讀 157,490評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵菜循,是天一觀的道長(zhǎng)翘地。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么衙耕? 我笑而不...
    開(kāi)封第一講書人閱讀 56,521評(píng)論 1 284
  • 正文 為了忘掉前任昧穿,我火速辦了婚禮,結(jié)果婚禮上橙喘,老公的妹妹穿的比我還像新娘时鸵。我一直安慰自己,他們只是感情好厅瞎,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,627評(píng)論 6 386
  • 文/花漫 我一把揭開(kāi)白布饰潜。 她就那樣靜靜地躺著,像睡著了一般磁奖。 火紅的嫁衣襯著肌膚如雪囊拜。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書人閱讀 49,842評(píng)論 1 290
  • 那天比搭,我揣著相機(jī)與錄音,去河邊找鬼南誊。 笑死身诺,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的抄囚。 我是一名探鬼主播霉赡,決...
    沈念sama閱讀 38,997評(píng)論 3 408
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼幔托!你這毒婦竟也來(lái)了穴亏?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書人閱讀 37,741評(píng)論 0 268
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤重挑,失蹤者是張志新(化名)和其女友劉穎嗓化,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體谬哀,經(jīng)...
    沈念sama閱讀 44,203評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡刺覆,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,534評(píng)論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了史煎。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片谦屑。...
    茶點(diǎn)故事閱讀 38,673評(píng)論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖篇梭,靈堂內(nèi)的尸體忽然破棺而出氢橙,到底是詐尸還是另有隱情,我是刑警寧澤恬偷,帶...
    沈念sama閱讀 34,339評(píng)論 4 330
  • 正文 年R本政府宣布悍手,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏谓苟。R本人自食惡果不足惜官脓,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,955評(píng)論 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望涝焙。 院中可真熱鬧卑笨,春花似錦、人聲如沸仑撞。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 30,770評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)隧哮。三九已至桶良,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間沮翔,已是汗流浹背陨帆。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 32,000評(píng)論 1 266
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留采蚀,地道東北人疲牵。 一個(gè)月前我還...
    沈念sama閱讀 46,394評(píng)論 2 360
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像榆鼠,于是被迫代替她去往敵國(guó)和親纲爸。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,562評(píng)論 2 349

推薦閱讀更多精彩內(nèi)容