第 12 期 golang 中 goroutine 的排程
文章來自於: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,並不是直接執行,而是放入可執行的佇列中,什麼時候執行用於並不能決定,而是搞排程系統去自發的執行。
更多原創文章乾貨分享,請關注公眾號
- 加微信實戰群請加微信(註明:實戰群):gocnio
相關文章
- goroutine 排程器(scheduler)Go
- Golang原始碼學習:排程邏輯(二)main goroutine的建立Golang原始碼AI
- 也談goroutine排程器Go
- Go1.12將支援搶佔式goroutine排程Go
- Go語言排程器之排程main goroutine(14)GoAI
- Golang —— goroutine(協程)和channel(管道)Golang
- PostgreSQL技術週刊第12期:PostgreSQL時空資料排程實踐SQL
- goroutine排程原始碼閱讀筆記Go原始碼筆記
- 理解 Go 中的協程(Goroutine)Go
- 第三章 Goroutine排程策略(16)Go
- go 原始碼分析 goroutine 概覽與排程Go原始碼
- Go語言goroutine排程器初始化Go
- Go語言排程器之盜取goroutine(17)Go
- Golang協程無法固定goroutine的最大數目解決Golang
- [Golang基礎]GoroutineGolang
- 關於golang的goroutine schedulerGolang
- Go runtime 排程器精講(三):main goroutine 建立GoAI
- 第3講:程序排程
- [典藏版] Golang 排程器 GMP 原理與排程全分析Golang
- linux系統中的排程週期任務:cronLinux
- golang如何結束goroutineGolang
- Go runtime 排程器精講(四):執行 main goroutineGoAI
- Golang的GMP排程模型與原始碼解析Golang模型原始碼
- Golang 的 goroutine 是如何實現的?Golang
- Golang語言goroutine協程併發安全及鎖機制Golang
- Golang 的 協程排程機制 與 GOMAXPROCS 效能調優Golang
- 緊湊迴圈中無法發生goroutine排程的問題終於要解決了Go
- Go runtime 排程器精講(六):非 main goroutine 執行GoAI
- [譯] React 中的排程React
- golang 原始碼分析之scheduler排程器Golang原始碼
- 每週分享第 12 期
- iTran樂譯第12期
- 第 64 期深入淺出 Golang RuntimeGolang
- Golang 獲取 goroutine id 完全指南Golang
- Golang1.7 Goroutine原始碼分析Golang原始碼
- java週期排程幾種實現Java
- Goroutine被動排程之一(18)Go
- 在 Golang 中進行「任務排程」,像用 Laravel 一樣絲滑GolangLaravel