Go并發(fā)調(diào)度進(jìn)階-【公粽號:堆棧future】
2. GMP初始化
1. M的初始化
M 只有自旋和非自旋兩種狀態(tài)出刷。自旋的時(shí)候睡汹,會(huì)努力找工作厨幻;找不到的時(shí)候會(huì)進(jìn)入非自旋狀態(tài),之后會(huì)休眠屎暇,直到有工作需要處理時(shí)承桥,被其他工作線程喚醒,又進(jìn)入自旋狀態(tài)根悼。
<pre data-tool="mdnice編輯器" style="box-sizing: border-box !important; margin: 10px 0px; padding: 0px; outline: 0px; font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; font-size: 14px; overflow: auto; display: block; color: rgb(33, 37, 41); max-width: 100%; overflow-wrap: break-word !important; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: 0.544px; orphans: 2; text-align: left; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(255, 255, 255); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;">`// src/runtime/proc.go
func mcommoninit(mp *m, id int64) {
g := getg()
...
lock(&sched.lock)
...
//random初始化凶异,用于竊取 G
mp.fastrand[0] = uint32(int64Hash(uint64(mp.id), fastrandseed))
mp.fastrand[1] = uint32(int64Hash(uint64(cputicks()), ^fastrandseed))
if mp.fastrand[0]|mp.fastrand[1] == 0 {
mp.fastrand[1] = 1
}
// 創(chuàng)建用于信號處理的gsignal蜀撑,只是簡單的從堆上分配一個(gè)g結(jié)構(gòu)體對象,然后把棧設(shè)置好就返回了
mpreinit(mp)
if mp.gsignal != nil {
mp.gsignal.stackguard1 = mp.gsignal.stack.lo + _StackGuard
}
// 把 M 掛入全局鏈表allm之中
mp.alllink = allm
...
}` </pre>
這里傳入的 id 是-1,初次調(diào)用會(huì)將 id 設(shè)置為 0唠帝,這里并未對M0做什么關(guān)于調(diào)度相關(guān)的初始化屯掖,所以可以簡單的認(rèn)為這個(gè)函數(shù)只是把M0放入全局鏈表allm之中就返回了玄柏。當(dāng)然這個(gè)M0就是主M襟衰。
2. P的初始化
通常情況下(在程序運(yùn)行時(shí)不調(diào)整 P 的個(gè)數(shù)),P 只會(huì)在上圖中的四種狀態(tài)下進(jìn)行切換粪摘。當(dāng)程序剛開始運(yùn)行進(jìn)行初始化時(shí)瀑晒,所有的 P 都處于
_Pgcstop
狀態(tài), 隨著 P 的初始化(runtime.procresize)徘意,會(huì)被置于_Pidle
苔悦。當(dāng) M 需要運(yùn)行時(shí),會(huì) runtime.acquirep 來使 P 變成
Prunning
狀態(tài)椎咧,并通過 runtime.releasep 來釋放玖详。當(dāng) G 執(zhí)行時(shí)需要進(jìn)入系統(tǒng)調(diào)用,P 會(huì)被設(shè)置為
_Psyscall
勤讽, 如果這個(gè)時(shí)候被系統(tǒng)監(jiān)控?fù)寠Z(runtime.retake)蟋座,則 P 會(huì)被重新修改為_Pidle
。如果在程序運(yùn)行中發(fā)生 GC脚牍,則 P 會(huì)被設(shè)置為
_Pgcstop
向臀, 并在 runtime.startTheWorld 時(shí)重新調(diào)整為_Prunning
。
<pre data-tool="mdnice編輯器" style="box-sizing: border-box !important; margin: 10px 0px; padding: 0px; outline: 0px; font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; font-size: 14px; overflow: auto; display: block; color: rgb(33, 37, 41); max-width: 100%; overflow-wrap: break-word !important; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: 0.544px; orphans: 2; text-align: left; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(255, 255, 255); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;">`var allp []*p
func procresize(nprocs int32) p {
// 獲取先前的 P 個(gè)數(shù)
old := gomaxprocs
// 更新統(tǒng)計(jì)信息
now := nanotime()
if sched.procresizetime != 0 {
sched.totaltime += int64(old) * (now - sched.procresizetime)
}
sched.procresizetime = now
// 根據(jù) runtime.MAXGOPROCS 調(diào)整 p 的數(shù)量,因?yàn)?runtime.MAXGOPROCS 用戶可以自行設(shè)定
if nprocs > int32(len(allp)) {
lock(&allpLock)
if nprocs <= int32(cap(allp)) {
allp = allp[:nprocs]
} else {
nallp := make([]p, nprocs)
copy(nallp, allp[:cap(allp)])
allp = nallp
}
unlock(&allpLock)
}
// 初始化新的 P
for i := old; i < nprocs; i++ {
pp := allp[i]
// 為空,則申請新的 P 對象
if pp == nil {
pp = new(p)
}
pp.init(i)
atomicstorep(unsafe.Pointer(&allp[i]), unsafe.Pointer(pp))
}
g := getg()
// P 不為空,并且 id 小于 nprocs ,那么可以繼續(xù)使用當(dāng)前 P
if g.m.p != 0 && g.m.p.ptr().id < nprocs {
// continue to use the current P
g.m.p.ptr().status = _Prunning
g.m.p.ptr().mcache.prepareForSweep()
} else {
// 釋放當(dāng)前 P诸狭,因?yàn)橐咽?br>
if g.m.p != 0 {
g.m.p.ptr().m = 0
}
g.m.p = 0
p := allp[0]
p.m = 0
p.status = _Pidle
// P0 綁定到當(dāng)前的 M0
acquirep(p)
}
// 從未使用的 P 釋放資源
for i := nprocs; i < old; i++ {
p := allp[i]
p.destroy()
// 不能釋放 p 本身券膀,因?yàn)樗赡茉?m 進(jìn)入系統(tǒng)調(diào)用時(shí)被引用
}
// 釋放完 P 之后重置allp的長度
if int32(len(allp)) != nprocs {
lock(&allpLock)
allp = allp[:nprocs]
unlock(&allpLock)
}
var runnablePs *p
// 將沒有本地任務(wù)的 P 放到空閑鏈表中
for i := nprocs - 1; i >= 0; i-- {
p := allp[i]
// 當(dāng)前正在使用的 P 略過
if g.m.p.ptr() == p {
continue
}
// 設(shè)置狀態(tài)為 _Pidle
p.status = _Pidle
// P 的任務(wù)列表是否為空
if runqempty(p) {
// 放入到空閑列表中
pidleput(p)
} else {
// 獲取空閑 M 綁定到 P 上
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
}` </pre>
procresize方法的執(zhí)行過程如下:
- allp 是全局變量 P 的資源池,如果 allp 的切片中的處理器數(shù)量少于期望數(shù)量驯遇,會(huì)對切片進(jìn)行擴(kuò)容芹彬;
- 擴(kuò)容的時(shí)候會(huì)使用 new 申請一個(gè)新的 P ,然后使用 init 初始化叉庐,需要注意的是初始化的 P 的 id 就是傳入的 i 的值雀监,狀態(tài)為 _Pgcstop;
- 然后通過 g.m.p 獲取 M0眨唬,如果 M0 已與有效的 P 綁定上会前,則將 被綁定的 P 的狀態(tài)修改為 _Prunning。否則獲取 allp[0] 作為 P0 調(diào)用 runtime.acquirep 與 M0 進(jìn)行綁定匾竿;
- 超過處理器個(gè)數(shù)的 P 通過p.destroy釋放資源瓦宜,p.destroy會(huì)將與 P 相關(guān)的資源釋放,并將 P 狀態(tài)設(shè)置為 _Pdead岭妖;
- 通過截?cái)喔淖內(nèi)肿兞?allp 的長度保證與期望處理器數(shù)量相等临庇;
- 遍歷 allp 檢查 P 的是否處于空閑狀態(tài)反璃,是的話放入到空閑列表中;
3. G的初始化
這是G的狀態(tài)流轉(zhuǎn)圖假夺,G的初始化相當(dāng)復(fù)雜淮蜈,需要大家下去對照源碼再看一遍。
<pre data-tool="mdnice編輯器" style="box-sizing: border-box !important; margin: 10px 0px; padding: 0px; outline: 0px; font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; font-size: 14px; overflow: auto; display: block; color: rgb(33, 37, 41); max-width: 100%; overflow-wrap: break-word !important; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: 0.544px; orphans: 2; text-align: left; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(255, 255, 255); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;">`func newproc(siz int32, fn *funcval) {
//從 fn 的地址增加一個(gè)指針的長度已卷,從而獲取第一參數(shù)地址
argp := add(unsafe.Pointer(&fn), sys.PtrSize)
gp := getg()
pc := getcallerpc() // 獲取調(diào)用方 PC/IP 寄存器值
// 用 g0 系統(tǒng)棧創(chuàng)建 Goroutine 對象
// 傳遞的參數(shù)包括 fn 函數(shù)入口地址, argp 參數(shù)起始地址, siz 參數(shù)長度, gp(g0)梧田,調(diào)用方 pc(goroutine)
systemstack(func() {
newg := newproc1(fn, argp, siz, gp, pc)
p := getg().m.p.ptr() //獲取p
runqput(p, newg, true) //并將其放入 P 本地隊(duì)列的隊(duì)頭或全局隊(duì)列
//檢查空閑的 P,將其喚醒侧蘸,準(zhǔn)備執(zhí)行 G裁眯,但我們目前處于初始化階段,主 Goroutine 尚未開始執(zhí)行讳癌,因此這里不會(huì)喚醒 P穿稳。
if mainStarted {
wakep()
}
})
}
// 創(chuàng)建一個(gè)運(yùn)行 fn 的新 g,具有 narg 字節(jié)大小的參數(shù)晌坤,從 argp 開始逢艘。
// callerps 是 go 語句的起始地址。新創(chuàng)建的 g 會(huì)被放入 g 的隊(duì)列中等待運(yùn)行骤菠。
func newproc1(fn *funcval, argp unsafe.Pointer, narg int32, callergp *g, callerpc uintptr) *g {
g := getg()//因?yàn)槭窃谙到y(tǒng)棧運(yùn)行所以此時(shí)的 g 為 g0
acquirem() //禁止搶占
if fn == nil {
g.m.throwing = -1 // do not dump full stacks
throw("go of nil func value")
}
...
siz := narg
siz = (siz + 7) &^ 7
...
// 當(dāng)前工作線程所綁定的 p
// 初始化時(shí) p = g0.m.p它改,也就是 p = allp[0]
p := g.m.p.ptr()
// 從 p 的本地緩沖里獲取一個(gè)沒有使用的 g,初始化時(shí)為空娩怎,返回 nil
newg := gfget(p)
if newg == nil {
// 創(chuàng)建一個(gè)擁有 _StackMin 大小的棧的 g
newg = malg(_StackMin)
// 將新創(chuàng)建的 g 從 _Gidle 更新為 _Gdead 狀態(tài)
casgstatus(newg, _Gidle, _Gdead)
// 將 Gdead 狀態(tài)的 g 添加到 allg搔课,這樣 GC 不會(huì)掃描未初始化的棧
allgadd(newg)
}
if newg.stack.hi == 0 {
throw("newproc1: newg missing stack")
}
if readgstatus(newg) != _Gdead {
throw("newproc1: new g is not Gdead")
}
...
// 確定 sp 位置
sp := newg.stack.hi - totalSize
// 確定參數(shù)入棧位置
spArg := sp
...
if narg > 0 {
// 將參數(shù)從執(zhí)行 newproc 函數(shù)的棧拷貝到新 g 的棧
memmove(unsafe.Pointer(spArg), argp, uintptr(narg))
...
}
// 設(shè)置newg的調(diào)度相關(guān)信息
memclrNoHeapPointers(unsafe.Pointer(&newg.sched), unsafe.Sizeof(newg.sched))
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.ancestors = saveAncestors(callergp)
newg.startpc = fn.fn
if g.m.curg != nil {
newg.labels = g.m.curg.labels
}
if isSystemGoroutine(newg, false) {
atomic.Xadd(&sched.ngsys, +1)
}
// 設(shè)置 g 的狀態(tài)為 _Grunnable截亦,可以運(yùn)行了
casgstatus(newg, _Gdead, _Grunnable)
// 設(shè)置goid
newg.goid = int64(p.goidcache)
p.goidcache++
...
releasem(g.m) //恢復(fù)搶占 本質(zhì)上是加鎖
return newg
}` </pre>
創(chuàng)建 G 的過程也是相對比較復(fù)雜的爬泥,我們來總結(jié)一下這個(gè)過程:
- 首先嘗試從 P 本地 gfree 鏈表或全局 gfree 隊(duì)列獲取已經(jīng)執(zhí)行過的 g
- 初始化過程中程序無論是本地隊(duì)列還是全局隊(duì)列都不可能獲取到 g,因此創(chuàng)建一個(gè)新的 g崩瓤,并為其分配運(yùn)行線程(執(zhí)行棧)袍啡,這時(shí) g 處于 _Gidle 狀態(tài)
- 創(chuàng)建完成后,g 被更改為 _Gdead 狀態(tài)却桶,并根據(jù)要執(zhí)行函數(shù)的入口地址和參數(shù)境输,初始化執(zhí)行棧的 SP 和參數(shù)的入棧位置,并將需要的參數(shù)拷貝一份存入執(zhí)行棧中
- 根據(jù) SP颖系、參數(shù)嗅剖,在 g.sched 中保存 SP 和 PC 指針來初始化 g 的運(yùn)行現(xiàn)場
- 將調(diào)用方、要執(zhí)行的函數(shù)的入口 PC 進(jìn)行保存嘁扼,并將 g 的狀態(tài)更改為 _Grunnable
- 給 Goroutine 分配 id信粮,并將其放入 P 本地隊(duì)列的隊(duì)頭或全局隊(duì)列(初始化階段隊(duì)列肯定不是滿的,因此不可能放入全局隊(duì)列)
- 檢查空閑的 P趁啸,將其喚醒强缘,準(zhǔn)備執(zhí)行 G督惰,但我們目前處于初始化階段,主 Goroutine 尚未開始執(zhí)行旅掂,因此這里不會(huì)喚醒 P赏胚。
4. 小結(jié)
結(jié)合上篇GMP結(jié)構(gòu)和這篇的初始化,那么對于GMP的調(diào)度我相信會(huì)有一定的理解商虐,起碼你知道了底層的一些點(diǎn)點(diǎn)滴滴觉阅,后續(xù)文章還會(huì)給大家繼續(xù)講解GMP調(diào)度,你堅(jiān)持學(xué)習(xí)到最后發(fā)現(xiàn)称龙,最難啃的GMP也就那么回事留拾!