Scheduler structures
Gs, Ms, Ps
Scheduler 管理 runtime 中三種類型的資源 : Gs, Ms, Ps
G : 代表「goroutine」,型別
g- 當一個 goroutine 離開時,它的
g物件會放回到可用的gs pool 中,讓之後的其他 goroutine 可以重複使用
- 當一個 goroutine 離開時,它的
M : 代表「OS thread」,型別
m- 可用來執行 user Go code, runtime code, system call 或是狀態 idle
P : 代表「執行 user Go code 所需的資源」,型別
p- 例如 scheduler 或 memory allocator state
- 總共有
GOMAXPROCS數量的 Ps - P 可以想做是 OS scheduler 中的 CPU,p 的內容可以想成是 per-CPU state
g, m, p 分配在 heap 中,但它們並不會被釋放 (free) 掉,所以它們是 type stable。
getg() and getg().m.curg
getg() 會回傳當前的 g,但若是正在執行 syscall 或 signal handler,則會回傳 g0 或是 gsignal (兩個都在 m 裡面)。
因此要取得當前的 user g,需使用 getg().m.curg。
TIP可透過
getg() == getg().m.curg來判斷是執行在 user stask 還是 system stack 上。
gopark
goroutine 等待與喚醒流程
以我們上一篇提到的 hchan 為例 :

Source Code
// go/src/runtime/proc.go
func gopark(unlockf func(*g, unsafe.Pointer) bool, lock unsafe.Pointer, reason waitReason, traceReason traceBlockReason, traceskip int) {
if reason != waitReasonSleep {
checkTimeouts() // timeouts may expire while two goroutines keep the scheduler busy
}
mp := acquirem()
gp := mp.curg()
status := readgstatus(gp)
if status != _Grunning && status != Gscanrunning {
throw("gopark: bad g status")
}
mp.waitlock = lock
mp.waitunlockf = unlockf
gp.waitreason = reason
mp.waitTraceBlockReason = traceReason
mp.waitTraceSkip = traceskip
releasem(mp)
// can't do anything that might move the G between Ms here.
mcall(park_m)
}gopark 輸入參數的部分 :
unlockf: park 前要執行的 unlock functionlock: 傳給unlockf的資料指標,通常是鎖本身reason: 鎖住原因,方便 debug
執行流程
acquirem()取得目前的 M// go/src/runtime/runtime1.go // go:nosplit func acquirem() *m { gp := getg() gp.m.locks++ return gp.m }mp.curg是目前執行的 G確認目前 goroutine 的狀態是執行中的
把
lock跟unlockf註冊到的 M,並把其他資訊記錄下來releasem(mp)釋放 M 的鎖// go/src/runtime/runtime1.go // go:nosplit func releasem(mp *m) { gp := getg() mp.locks-- if mp.locks == 0 && gp.preempt { // restore the preemption request in case we've cleared it in newstack gp.stackguard0 = stackPreempt } }透過
mcall進入 system stack 呼叫park_m,park_m會真正執行把 goroutine 放到 wait queue 中,並執行unlockf
mcall and park_m
前面我們提到,gopark 在最後呼叫 mcall(park_m) 來執行實際的 park 行為。這裡,我們來仔細看內部到底做了哪些事。
mcall
// go/src/runtime/stubs.go
// mcall switches from the g to the g0 stack and invokes fn(g),
// where g is the goroutine that made the call.
// mcall saves g's current PC/SP in g->sched so that it can be restored later.
// It is up to fn to arrange for that later execution, typically by recording
// g in a data structure, causing something to call ready(g) later.
// mcall returns to the original goroutine g later, when g has been rescheduled.
// fn must not return at all; typically it ends by calling schedule, to let the m
// run other goroutines.
//
// mcall can only be called from g stacks (not g0, not gsignal).
//
// This must NOT be go:noescape: if fn is a stack-allocated closure,
// fn puts g on a run queue, and g executes before fn returns, the
// closure will be invalidated while it is still executing.
func mcall(fn func(*g))總結來說,mcall 將 goroutine (user stack) 切換到 g0 (system stack),然後在 g0 上執行 fn(g)。
park_m
// park continuation on g0.
func park_m(gp *g) {
mp := getg().m
trace := traceAcquire()
// If g is in a synctest group, we don't want to let the group
// become idle until after the waitunlockf (if any) has confirmed
// that the park is happening.
// We need to record gp.bubble here, since waitunlockf can change it.
bubble := gp.bubble
if bubble != nil {
bubble.incActive()
}
if trace.ok() {
// Trace the event before the transition. It may take a
// stack trace, but we won't own the stake after the
// transition anymore.
trace.GoPark(mp.waitTraceBlockReason, mp.waitTraceSkip)
}
// N.B. Not using casGToWaiting here because the waitreason is
// set by park_m's caller.
casgstatus(gp, _Grunning, _Gwaiting)
if trace.ok() {
traceRelease(trace)
}
dropg()
if fn := mp.waitunlockf; fn != nil {
ok := fn(gp, mp.waitlock)
mp.waitunlockf = nil
mp.waitloc = nil
if !ok {
trace := traceAcquire()
casgstatus(gp, _Gwaiting, _Grunnable)
if bubble != nil {
bubble.decActive()
}
if trace.ok() {
trace.GoUnpark(gp, 2)
traceRelease(trace)
}
execute(gp, true) // Schedule it back, never returns.
}
}
if bubble != nil {
bubble.decActive()
}
schedule()
}重點整理 :
casgstatus(gp, _Grunning, _Gwaiting)把這個 goroutine 狀態從執行中設為等待中dropg()拋棄目前的 goroutine,代表這個 M 不再屬於該 goroutine,可以跑其他 goroutine 了- 如果有
unlockf,ok == true: 繼續進行scheduler()ok == false: 阻塞失敗,恢復 goroutine 為_Grunnable,並呼叫execute讓它重跑
- 目前 goroutine 成功 park,呼叫
scheduler()讓 M 去執行別的 G
以上,大概就是這次介紹的 gopark 內容,我們後會有期 XD