第 12 期 golang 中 goroutine 的排程

mai_yang發表於2020-02-13

文章來自於:https://reading.developerlearning.cn/reading/12-2018-08-02-goroutine-gpm/

觀看視訊

鄭寶楊 (boya) 2018-08-01 listomebao@gmail.com

閱讀原始碼前可以閱讀的資料

golang 的排程模型概覽

排程的機制用一句話描述:
runtime 準備好 G,P,M,然後 M 繫結 P,M 從各種佇列中獲取 G,切換到 G 的執行棧上並執行 G 上的任務函式,呼叫 goexit 做清理工作並回到 M,如此反覆。

基本概念

M(machine)

  • M 代表著真正的執行計算資源,可以認為它就是 os thread(系統執行緒)。
  • M 是真正排程系統的執行者,每個 M 就像一個勤勞的工作者,總是從各種佇列中找到可執行的 G,而且這樣 M 的可以同時存在多個。
  • M 在繫結有效的 P 後,進入排程迴圈,而且 M 並不保留 G 狀態,這是 G 可以跨 M 排程的基礎。

P(processor)

  • P 表示邏輯 processor,是執行緒 M 的執行的上下文。
  • P 的最大作用是其擁有的各種 G 物件佇列、連結串列、cache 和狀態。

G(goroutine)

  • 排程系統的最基本單位 goroutine,儲存了 goroutine 的執行 stack 資訊、goroutine 狀態以及 goroutine 的任務函式等。
  • 在 G 的眼中只有 P,P 就是執行 G 的 “CPU”。
  • 相當於兩級執行緒

執行緒實現模型

來自Go併發程式設計實戰

                    +-------+       +-------+      
                    |  KSE  |       |  KSE  |          
                    +-------+       +-------+      
                        |               |                       核心空間
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -        
                        |               |                       使用者空間
                    +-------+       +-------+
                    |   M   |       |   M   |
                    +-------+       +-------+
                  |          |         |          |
              +------+   +------+   +------+   +------+            
              |   P  |   |   P  |   |   P  |   |   P  |
              +------+   +------+   +------+   +------+   
           |     |     |     |     |     |     |     |     | 
         +---+ +---+ +---+ +---+ +---+ +---+ +---+ +---+ +---+ 
         | G | | G | | G | | G | | G | | G | | G | | G | | G | 
         +---+ +---+ +---+ +---+ +---+ +---+ +---+ +---+ +---+ 
  • KSE(Kernel Scheduling Entity)是核心排程實體
  • M 與 P,P 與 G 之前的關聯都是動態的,可以變的

關係示意圖

來自golang原始碼剖析

                            +-------------------- sysmon ---------------//------+ 
                            |                                                   |
                            |                                                   |
               +---+      +---+-------+                   +--------+          +---+---+
go func() ---> | G | ---> | P | local | <=== balance ===> | global | <--//--- | P | M |
               +---+      +---+-------+                   +--------+          +---+---+
                            |                                 |                 | 
                            |      +---+                      |                 |
                            +----> | M | <--- findrunnable ---+--- steal <--//--+
                                   +---+ 
                                     |
                                   mstart
                                     |
              +--- execute <----- schedule 
              |                      |   
              |                      |
              +--> G.fn --> goexit --+ 


              1. go func() 語氣建立G。
              2. 將G放入P的本地佇列(或者平衡到全域性全域性佇列)。
              3. 喚醒或新建M來執行任務。
              4. 進入排程迴圈
              5. 盡力獲取可執行的G,並執行
              6. 清理現場並且重新進入排程迴圈

GPM 的來由

特殊的 g0 和 m0

g0 和 m0 是在proc.go檔案中的兩個全域性變數,m0 就是程式啟動後的初始執行緒,g0 也是代表著初始執行緒的 stack
asm_amd64.go --> runtime·rt0_go(SB)

// 程式剛啟動的時候必定有一個執行緒啟動(主執行緒)
// 將當前的棧和資源儲存在g0
// 將該執行緒儲存在m0
// tls: Thread Local Storage
// set the per-goroutine and per-mach "registers"
get_tls(BX)
LEAQ    runtime·g0(SB), CX
MOVQ    CX, g(BX)
LEAQ    runtime·m0(SB), AX

// save m->g0 = g0
MOVQ    CX, m_g0(AX)
// save m0 to g0->m
MOVQ    AX, g_m(CX)

M 的一生

M 的建立

proc.go

// Create a new m. It will start off with a call to fn, or else the scheduler.
// fn needs to be static and not a heap allocated closure.
// May run with m.p==nil, so write barriers are not allowed.
//go:nowritebarrierrec
// 建立一個新的m,它將從fn或者排程程式開始
func newm(fn func(), _p_ *p) {
    // 根據fn和p和繫結一個m物件
    mp := allocm(_p_, fn)
    // 設定當前m的下一個p為_p_
    mp.nextp.set(_p_)
    mp.sigmask = initSigmask
    ...
    // 真正的分配os thread
    newm1(mp)
}
func newm1(mp *m) {
    // 對cgo的處理
    ...
    execLock.rlock() // Prevent process clone.
    // 建立一個系統執行緒
    newosproc(mp, unsafe.Pointer(mp.g0.stack.hi))
    execLock.runlock()
}

狀態

 mstart
    |
    v        找不到可執行任務,gc STW,
+------+     任務執行時間過長,系統阻塞等   +------+
| spin | ----------------------------> |unspin| 
+------+          mstop                +------+
    ^                                      |
    |                                      v
notewakeup <-------------------------  notesleep

M 的一些問題

https://github.com/golang/go/issues/14592

P 的一生

P 的建立

proc.go

// Change number of processors. The world is stopped, sched is locked.
// gcworkbufs are not being modified by either the GC or
// the write barrier code.
// Returns list of Ps with local work, they need to be scheduled by the caller.
// 所有的P都在這個函式分配,不管是最開始的初始化分配,還是後期調整
func procresize(nprocs int32) *p {
    old := gomaxprocs
    // 如果 gomaxprocs <=0 丟擲異常
    if old < 0 || nprocs <= 0 {
        throw("procresize: invalid arg")
    }
  ...
    // Grow allp if necessary.
    if nprocs > int32(len(allp)) {
        // Synchronize with retake, which could be running
        // concurrently since it doesn't run on a P.
        lock(&allpLock)
        if nprocs <= int32(cap(allp)) {
            allp = allp[:nprocs]
        } else {
            // 分配nprocs個*p
            nallp := make([]*p, nprocs)
            // Copy everything up to allp's cap so we
            // never lose old allocated Ps.
            copy(nallp, allp[:cap(allp)])
            allp = nallp
        }
        unlock(&allpLock)
    }

    // initialize new P's
    for i := int32(0); i < nprocs; i++ {
        pp := allp[i]
        if pp == nil {
            pp = new(p)
            pp.id = i
            pp.status = _Pgcstop            // 更改狀態
            pp.sudogcache = pp.sudogbuf[:0] //將sudogcache指向sudogbuf的起始地址
            for i := range pp.deferpool {
                pp.deferpool[i] = pp.deferpoolbuf[i][:0]
            }
            pp.wbBuf.reset()
            // 將pp儲存到allp陣列裡, allp[i] = pp
            atomicstorep(unsafe.Pointer(&allp[i]), unsafe.Pointer(pp))
        }
        ...
    }
  ...

    _g_ := getg()
    // 如果當前的M已經繫結P,繼續使用,否則將當前的M繫結一個P
    if _g_.m.p != 0 && _g_.m.p.ptr().id < nprocs {
        // continue to use the current P
        _g_.m.p.ptr().status = _Prunning
    } else {
        // release the current P and acquire allp[0]
        // 獲取allp[0]
        if _g_.m.p != 0 {
            _g_.m.p.ptr().m = 0
        }
        _g_.m.p = 0
        _g_.m.mcache = nil
        p := allp[0]
        p.m = 0
        p.status = _Pidle
        // 將當前的m和p繫結
        acquirep(p)
        if trace.enabled {
            traceGoStart()
        }
    }
    var runnablePs *p
    for i := nprocs - 1; i >= 0; i-- {
        p := allp[i]
        if _g_.m.p.ptr() == p {
            continue
        }
        p.status = _Pidle
        if runqempty(p) { // 將空閒p放入空閒連結串列
            pidleput(p)
        } else {
            p.m.set(mget())
            p.link.set(runnablePs)
            runnablePs = p
        }
    }
    stealOrder.reset(uint32(nprocs))
    var int32p *int32 = &gomaxprocs // make compiler check that gomaxprocs is an int32
    atomic.Store((*uint32)(unsafe.Pointer(int32p)), uint32(nprocs))
    return runnablePs
}

所有的 P 在程式啟動的時候就設定好了,並用一個 allp slice 維護,可以呼叫 runtime.GOMAXPROCS 調整 P 的個數,雖然代價很大

狀態轉換

                                            acquirep(p)        
                          不需要使用的P       P和M繫結的時候       進入系統呼叫       procresize()
new(p)  -----+        +---------------+     +-----------+     +------------+    +----------+
            |         |               |     |           |     |            |    |          |
            |   +------------+    +---v--------+    +---v--------+    +----v-------+    +--v---------+
            +-->|  _Pgcstop  |    |    _Pidle  |    |  _Prunning |    |  _Psyscall |    |   _Pdead   |
                +------^-----+    +--------^---+    +--------^---+    +------------+    +------------+
                       |            |     |            |     |            |
                       +------------+     +------------+     +------------+
                           GC結束            releasep()        退出系統呼叫
                                            P和M解綁                      

P 的數量預設等於 cpu 的個數,很多人認為 runtime.GOMAXPROCS 可以限制系統執行緒的數量,但這是錯誤的,M 是按需建立的,和 runtime.GOMAXPROCS 沒有關係。

G 的一生

G 的建立

proc.go

// Create a new g running fn with siz bytes of arguments.
// Put it on the queue of g's waiting to run.
// The compiler turns a go statement into a call to this.
// Cannot split the stack because it assumes that the arguments
// are available sequentially after &fn; they would not be
// copied if a stack split occurred.
//go:nosplit
// 新建一個goroutine,
// ? 用fn + PtrSize 獲取第一個引數的地址,也就是argp
// 用siz - 8 獲取pc地址
func newproc(siz int32, fn *funcval) {
    argp := add(unsafe.Pointer(&fn), sys.PtrSize)
    pc := getcallerpc()
    // 用g0的棧建立G物件
    systemstack(func() {
        newproc1(fn, (*uint8)(argp), siz, pc)
    })
}
// Create a new g running fn with narg bytes of arguments starting
// at argp. callerpc is the address of the go statement that created
// this. The new g is put on the queue of g's waiting to run.
// 根據函式引數和函式地址,建立一個新的G,然後將這個G加入佇列等待執行
func newproc1(fn *funcval, argp *uint8, narg int32, callerpc uintptr) {
    _g_ := getg()

    if fn == nil {
        _g_.m.throwing = -1 // do not dump full stacks
        throw("go of nil func value")
    }
    _g_.m.locks++ // disable preemption because it can be holding p in a local var
    siz := narg
    siz = (siz + 7) &^ 7

    // We could allocate a larger initial stack if necessary.
    // Not worth it: this is almost always an error.
    // 4*sizeof(uintreg): extra space added below
    // sizeof(uintreg): caller's LR (arm) or return address (x86, in gostartcall).
    // 如果函式的引數大小比2048大的話,直接panic
    if siz >= _StackMin-4*sys.RegSize-sys.RegSize {
        throw("newproc: function arguments too large for new goroutine")
    }

    // 從m中獲取p
    _p_ := _g_.m.p.ptr()
    // 從gfree list獲取g
    newg := gfget(_p_)
    // 如果沒獲取到g,則新建一個
    if newg == nil {
        newg = malg(_StackMin)
        casgstatus(newg, _Gidle, _Gdead) //將g的狀態改為_Gdead
        // 新增到allg陣列,防止gc掃描清除掉
        allgadd(newg) // publishes with a g->status of Gdead so GC scanner doesn't look at uninitialized stack.
    }
    if newg.stack.hi == 0 {
        throw("newproc1: newg missing stack")
    }

    if readgstatus(newg) != _Gdead {
        throw("newproc1: new g is not Gdead")
    }

    totalSize := 4*sys.RegSize + uintptr(siz) + sys.MinFrameSize // extra space in case of reads slightly beyond frame
    totalSize += -totalSize & (sys.SpAlign - 1)                  // align to spAlign
    sp := newg.stack.hi - totalSize
    spArg := sp
    if usesLR {
        // caller's LR
        *(*uintptr)(unsafe.Pointer(sp)) = 0
        prepGoExitFrame(sp)
        spArg += sys.MinFrameSize
    }
    if narg > 0 {
        // copy引數
        memmove(unsafe.Pointer(spArg), unsafe.Pointer(argp), uintptr(narg))
        // This is a stack-to-stack copy. If write barriers
        // are enabled and the source stack is grey (the
        // destination is always black), then perform a
        // barrier copy. We do this *after* the memmove
        // because the destination stack may have garbage on
        // it.
        if writeBarrier.needed && !_g_.m.curg.gcscandone {
            f := findfunc(fn.fn)
            stkmap := (*stackmap)(funcdata(f, _FUNCDATA_ArgsPointerMaps))
            // We're in the prologue, so it's always stack map index 0.
            bv := stackmapdata(stkmap, 0)
            bulkBarrierBitmap(spArg, spArg, uintptr(narg), 0, bv.bytedata)
        }
    }

    memclrNoHeapPointers(unsafe.Pointer(&newg.sched), unsafe.Sizeof(newg.sched))
    newg.sched.sp = sp
    newg.stktopsp = sp
    // 儲存goexit的地址到sched.pc
    newg.sched.pc = funcPC(goexit) + sys.PCQuantum // +PCQuantum so that previous instruction is in same function
    newg.sched.g = guintptr(unsafe.Pointer(newg))
    gostartcallfn(&newg.sched, fn)
    newg.gopc = callerpc
    newg.startpc = fn.fn
    if _g_.m.curg != nil {
        newg.labels = _g_.m.curg.labels
    }
    if isSystemGoroutine(newg) {
        atomic.Xadd(&sched.ngsys, +1)
    }
    newg.gcscanvalid = false
    // 更改當前g的狀態為_Grunnable
    casgstatus(newg, _Gdead, _Grunnable)

    if _p_.goidcache == _p_.goidcacheend {
        // Sched.goidgen is the last allocated id,
        // this batch must be [sched.goidgen+1, sched.goidgen+GoidCacheBatch].
        // At startup sched.goidgen=0, so main goroutine receives goid=1.
        _p_.goidcache = atomic.Xadd64(&sched.goidgen, _GoidCacheBatch)
        _p_.goidcache -= _GoidCacheBatch - 1
        _p_.goidcacheend = _p_.goidcache + _GoidCacheBatch
    }
    // 生成唯一的goid
    newg.goid = int64(_p_.goidcache)
    _p_.goidcache++
    if raceenabled {
        newg.racectx = racegostart(callerpc)
    }
    if trace.enabled {
        traceGoCreate(newg, newg.startpc)
    }
    // 將當前新生成的g,放入佇列
    runqput(_p_, newg, true)

    // 如果有空閒的p 且 m沒有處於自旋狀態 且 main goroutine已經啟動,那麼喚醒某個m來執行任務
    if atomic.Load(&sched.npidle) != 0 && atomic.Load(&sched.nmspinning) == 0 && mainStarted {
        wakep()
    }
    _g_.m.locks--
    if _g_.m.locks == 0 && _g_.preempt { // restore the preemption request in case we've cleared it in newstack
        _g_.stackguard0 = stackPreempt
    }
}

G 的狀態圖

                                                    +------------+
                                    ready           |            |
                                +------------------ |  _Gwaiting |
                                |                   |            |
                                |                   +------------+
                                |                         ^ park_m
                                V                         | 
+------------+            +------------+  execute   +------------+            +------------+    
|            |  newproc   |            | ---------> |            |   goexit   |            |
|  _Gidle    | ---------> | _Grunnable |  yield     | _Grunning  | ---------> |   _Gdead   |      
|            |            |            | <--------- |            |            |            |
+------------+            +-----^------+            +------------+            +------------+
                                |         entersyscall |      ^ 
                                |                      V      | existsyscall
                                |                   +------------+
                                |   existsyscall    |            |
                                +------------------ |  _Gsyscall |
                                                    |            |
                                                    +------------+

新建的 G 都是_Grunnable 的,新建 G 的時候優先從 gfree list 從獲取 G,這樣可以複用 G,所以上圖的狀態不是完整的,_Gdead 通過 newproc 會變為_Grunnable, 通過 go func() 的語法新建的 G,並不是直接執行,而是放入可執行的佇列中,什麼時候執行用於並不能決定,而是搞排程系統去自發的執行。

更多原創文章乾貨分享,請關注公眾號

更多原創文章乾貨分享,請關注公眾號
  • 第 12 期 golang 中 goroutine 的排程
  • 加微信實戰群請加微信(註明:實戰群):gocnio

相關文章