本文使用 golang 1.17 代碼,如有任何問題,還望指出。
線程、核心線程和使用者線程差別
- 線程:從核心角度來說并沒有線程這個概念。Linux 把所有的線程都當做程序來實作,核心也沒有特别的排程算法來處理線程。線程僅僅被視為一個與其他程序共享某些資源的程序,和程序一樣,每個線程也都是有自己的
,是以在核心中,線程看起來就是一個普通的程序。線程也被稱作輕量級程序,一個程序可以有多個線程,線程擁有自己獨立的棧,切換也由作業系統排程。在 Linux 上可以通過task_struct
方法或者pthread_create()
系統調用建立;clone()
- 核心線程:獨立運作在核心空間的标準程序,核心線程和普通線程的差別在于核心線程沒有獨立的位址空間;
- 使用者線程:也被稱作協程,是一種基于線程之上,但又比線程更加輕量級的存在,由使用者運作時來管理的,作業系統感覺不到,它的切換是由使用者程式自己控制的,但使用者線程也是由核心線程來運作的。Lua 和 Python 中的協程(coroutine)、Golang 的 goroutine 都屬于使用者級線程;
三者的關系如下所示:
在 Golang 中 goroutine 與線程的關系如下所示:
Golang 程式啟動時首先會建立程序,然後建立主線程,主線程會執行 runtime 初始化的一些代碼,包括排程器的初始化,然後會啟動排程器,排程器會不斷尋找需要運作的 goroutine 與核心線程綁定運作。
Golang 使用協程的原因
作業系統中雖然已經有了多線程、多程序來解決高并發的問題,但是在當今網際網路海量高并發場景下,對性能的要求也越來越苛刻,大量的程序/線程會出現記憶體占用高、CPU消耗多的問題,很多服務的改造與重構也是為了降本增效。
一個程序可以關聯多個線程,線程之間會共享程序的一些資源,比如記憶體位址空間、打開的檔案、程序基礎資訊等,每個線程也都會有自己的棧以及寄存器資訊等,線程相比程序更加輕量,而協程相對線程更加輕量,多個協程會關聯到一個線程,協程之間會共享線程的一些資訊,每個協程也會有自己的棧空間,是以也會更加輕量級。從程序到線程再到協程,其實是一個不斷共享,減少切換成本的過程。
Golang 使用協程主要有以下幾個原因:
- (1)核心線程建立與切換太重的問題:建立和切換都要進入到核心态,進入到核心态開銷較大,性能代價大,而協程切換不需要進入到核心态;
- (2)線程記憶體使用太重:建立一個核心線程預設棧大小為8M,而建立一個使用者線程即 goroutine 隻需要 2K 記憶體,當 goroutine 棧不夠用時也會自動增加;
- (3)goroutine 的排程更靈活,所有協程的排程、切換都發生在使用者态,沒有建立線程的開銷,即使出現某個協程運作阻塞時,線程上的其他協程也會被排程到其他線程上運作;
Goroutine 在程序記憶體空間中的分布
協程的本質其實就是可以被暫停以及可以被恢複運作的函數,建立一個 goroutine 時會在程序的堆區中配置設定一段空間,這段空間是用來儲存協程棧區的,當需要恢複協程的運作時再從堆區中出來複制出來恢複函數運作時狀态。
GPM 模型分析
在 Golang 代碼的曆史送出記錄中會發現很多代碼都是從 C 翻譯到 Go 的,在 go 1.4 前 runtime 中大量代碼是用 C 實作的,比如目前版本
proc.go
檔案中實作的很多排程相關的功能最開始都是用 C 實作的,後面用 Go 代碼進行了翻譯,如果需要了解 Golang 最開始的設計細節可以翻閱最早送出的 C 代碼。
commit b2cdf30eb6c4a76504956aaaad47df969274296b
Author: Russ Cox <[email protected]>
Date: Tue Nov 11 17:08:33 2014 -0500
[dev.cc] runtime: convert scheduler from C to Go
commit 15ced2d00832dd9129b4ee0ac53b5367ade24c13
Author: Russ Cox <[email protected]>
Date: Tue Nov 11 17:06:22 2014 -0500
[dev.cc] runtime: convert assembly files for C to Go transition
The main change is that #include "zasm_GOOS_GOARCH.h"
is now #include "go_asm.h" and/or #include "go_tls.h".
Also, because C StackGuard is now Go _StackGuard,
the assembly name changes from const_StackGuard to
const__StackGuard.
In asm_$GOARCH.s, add new function getg, formerly
implemented in C.
本文主要介紹目前排程器中的 GPM 模型,首先了解下 GPM 模型中三個元件的作用與聯系:
- G: Goroutine,即我們在 Go 程式中使用
關鍵字運作的函數;go
- M: Machine,或 worker thread,代表系統線程,M 是 runtime 中的一個對象,每建立一個 M 會同時建立一個系統線程并與該 M 進行綁定;
- P: Processor,類似于 CPU 核心的概念,隻有當 M 與一個 P 關聯後才能執行 Go 代碼;
G 運作時需要與 M 進行綁定,M 需要與 P 綁定,M 在數量上并不與 P 相等,這是因為 M 在運作 G 時會陷入系統調用或者因其他事情會被阻塞,M 不夠用時在 runtime 中會建立新的 M,是以随着程式的執行,M 的數量可能增長,而 P 在沒有使用者幹預的情況下,則會保持不變,G 的數量是由使用者代碼決定的。
GPM 三者的關聯如下所示:
- 全局隊列:存放等待運作的 G。
- P 的本地隊列:同全局隊列類似,存放的也是等待運作的 G,存的數量有限。建立 G 時,G 優先加入到 P 的本地隊列,如果隊列滿了,則會把本地隊列中一部分 G 移動到全局隊列。
- P 清單:所有的 P 都在程式啟動時建立,并儲存在數組中,最多有
(可配置) 個。GOMAXPROCS
- M:線程想運作任務就得擷取 P,然後從 P 的本地隊列擷取 G,P 隊列為空時,M 也會嘗試從全局隊列拿一批 G 放到 P 的本地隊列,或從其他 P 的本地隊列偷一半放到自己 P 的本地隊列,M 運作 G,G 執行之後,M 會從 P 擷取下一個 G,不斷重複下去。
GPM 生命周期
1、P 的生命周期
P 對象的結構體如下所示:
type p struct {
id int32
status uint32 // P 的狀态
link puintptr
schedtick uint32 // 被排程次數
syscalltick uint32 // 執行過系統調用的次數
sysmontick sysmontick // sysmon 最近一次運作的時間
m muintptr // P 關聯的 M
mcache *mcache // 小對象緩存,可以無鎖通路
pcache pageCache // 頁緩存,可以無鎖通路
raceprocctx uintptr // race相關
// 與 defer 相關
deferpool [5][]*_defer
deferpoolbuf [5][32]*_defer
// goroutine ids 的緩存
goidcache uint64
goidcacheend uint64
// P 本地 G 隊列,可以無鎖通路
runqhead uint32 // 本地隊列頭
runqtail uint32 // 本地隊尾
runq [256]guintptr // 本地 G 隊列,使用數組實作的循環隊列
runnext guintptr // 待運作的 G,優先級高于 runq
// 已運作結束的 G (狀态為 Gdead)會被儲存在 gFree 中,友善實作對 G 的複用
gFree struct {
gList
n int32
}
sudogcache []*sudog
sudogbuf [128]*sudog
mspancache struct {
len int
buf [128]*mspan
}
tracebuf traceBufPtr
traceSweep bool
traceSwept, traceReclaimed uintptr
palloc persistentAlloc
_ uint32
timer0When uint64
timerModifiedEarliest uint64
// 與 GC 相關的
gcAssistTime int64
gcFractionalMarkTime int64
gcMarkWorkerMode gcMarkWorkerMode
gcMarkWorkerStartTime int64
gcw gcWork
wbBuf wbBuf
......
// 搶占标記
preempt bool
}
(1) 為什麼需要 P ?
在 Golang 1.1 版本之前排程器中還沒有 P 元件,此時排程器的性能還比較差,社群的 Dmitry Vyukov 大佬針對目前排程器中存在的問題進行了總結并設計引入 P 元件來解決目前面臨的問題(
Scalable Go Scheduler Design Doc),并在 Go 1.1 版本中引入了 P 元件,引入 P 元件後不僅解決了文檔中列的幾個問題,也引入了一些很好的機制。
文檔中列出了排程器目前主要有 4 個問題,主要有:
- 1、全局互斥鎖 (
sched.Lock
) 問題:社群在測試中發現 Golang 程式在運作時有 14% 的 CPU 使用率消耗在對全局鎖的處理上。沒有 P 元件時,M 隻能通過加互斥鎖從全局隊列中擷取 G,在加鎖階段對其他 goroutine 處理時(建立,完成,重新排程等)會存在時延;
在引入 P 元件後,P 對象中會有一個隊列來儲存 G 清單,P 的本地隊列可以解決舊排程器中單一全局鎖的問題,而 G 隊列也被分成兩類,
中繼續保留全局 G 隊列,同時每個 P 中都會有一個本地的 G 隊列,此時 M 會優先運作 P 本地隊列中的 G,通路時也不需要加鎖。sched
-
2、G 切換問題:M 頻繁切換可運作的 G 會增加延遲和開銷,比如建立的 G 會被被放到全局隊列,而不是在 M 本地執行,這會導緻不必要的開銷和延遲,應該優先在建立 G 的 M 上執行就可以;
在引入 P 元件後,建立的 G 會優先放在 G 關聯 P 的本地隊列中。
- 3、M的記憶體緩存 (
) 問題:在還沒有 P 元件的版本中,每個 M 結構體都有一個M.mcache
字段,mcache
是一個記憶體配置設定池,小對象會直接從mcache
中進行配置設定,M 在運作 G 時,G 需要申請小對象時會直接從 M 的mcache
中進行配置設定,G 可以進行無鎖通路,因為每個 M 同一時間隻會運作一個 G,但 runtime 中每個時間隻會有一部分活躍的 M 在運作 G,其他因系統調用等阻塞的 M 其實不需要mcache
的,這部分mcache
是被浪費的,每個 M 的mcache
mcache
大概有 2M 大小的可用記憶體,當有上千個處于阻塞狀态的 M 時,會有大量的記憶體被消耗。此外還有較差的資料局部性問題,這是指 M 在運作 G 時對 G 所需要的小對象進行了緩存,後面 G 如果再次排程到同一個 M 時那麼可以加速通路,但在實際場景中 G 排程到同一個 M 的機率不高,是以資料局部性不太好。
在引入了 P 元件後,
從 M 轉移到了 P ,P 儲存了mcache
也就意味着不必為每一個 M 都配置設定 一個mcache
,避免了過多的記憶體消耗。這樣在高并發狀态下,每個 G 隻有在運作的時候才會使用到記憶體, 而每個 G 會綁定一個 P,是以隻有目前運作的 G 隻會占用一個mcache
,對于mcache
的數量就是 P 的數 量,同時并發通路時也不會産生鎖。mcache
- 4、線程頻繁阻塞與喚醒問題:在最初的排程器中,通過
runtime.GOMAXPROCS()
限制系統線程的數量,預設隻開啟一個系統線程。并且由于 M 會執行系統調用等操作,當 M 阻塞後不會建立 M 來執行其他的任務而是會等待 M 喚醒,M 會在阻塞與喚醒之間頻繁切換會導緻額外的開銷;
在新的排程器中,當 M 處于系統排程狀态時會和綁定的 P 解除關聯,會喚醒已有的或建立新的 M 來和 P 綁定運作其他的 G。
(2) P 的新增邏輯
P 的數量是在 runtime 啟動時初始化的,預設等于 cpu 的邏輯核數,在程式啟動時可以通過環境變量
GOMAXPROCS
或者
runtime.GOMAXPROCS()
方法進行設定,程式在運作過程中 P 的數量是固定不變的。
在 IO 密集型場景下,可以适當調高 P 的數量,因為 M 需要與 P 綁定才能運作,而 M 在執行 G 時某些操作會陷入系統調用,此時與 M 關聯的 P 處于等待狀态,如果系統調用一直不傳回那麼等待系統調用這段時間的 CPU 資源其實是被浪費的,雖然 runtime 中有
sysmon
監控線程可以搶占 G,此處就是搶占與 G 關聯的 P,讓 P 重新綁定一個 M 運作 G,但
sysmon
是周期性執行搶占的,在
sysmon
穩定運作後每隔 10ms 檢查一次是否要搶占 P,作業系統中在 10ms 内可以執行多次線程切換,如果 P 處于系統調用狀态還有需要運作的 G,這部分 G 得不到執行其實CPU資源是被浪費的。在一些項目中能看到有修改 P 數量的操作,開源資料庫項目
https://github.com/dgraph-io/dgraph中将
GOMAXPROCS
調整到 128 來增加 IO 處理能力。
(3) P 的銷毀邏輯
程式運作過程中如果沒有調整
GOMAXPROC
,未使用的 P 會放在排程器的全局隊列
schedt.pidle
,不會被銷毀。若調小了
GOMAXPROC
,通過
p.destroy()
會将多餘的 P 關聯的資源回收掉并且會将 P 狀态設定為
_Pdead
,此時可能還有與 P 關聯的 M 是以 P 對象不會被回收。
(4) P 的狀态
狀态 | 描述 |
---|---|
| P 被初始化後的狀态,此時還沒有運作使用者代碼或者排程器 |
| P 被 M 綁定并且運作使用者代碼時的狀态 |
| 當 G 被執行時需要進入系統調用時,P 會被關聯的 M 設定為該狀态 |
| 在程式運作中發生 GC 時,P 會被關聯的 M 設定為該狀态 |
| 程式在運作過程中如果将 數量減少時,此時多餘的 P 會被設定為 |
2、M 的生命周期
M 對象的的結構體為:
type m struct {
// g0 記錄工作線程(也就是核心線程)使用的棧資訊,在執行排程代碼時需要使用
g0 *g
morebuf gobuf // 堆棧擴容使用
......
gsignal *g // 用于信号處理
......
// 通過 tls (線程本地存儲)結構體實作 m 與工作線程的綁定
tls [tlsSlots]uintptr
mstartfn func() // 表示m啟動時立即執行的函數
curg *g // 指向正在運作的 goroutine 對象
caughtsig guintptr
p puintptr // 目前 m 綁定的 P
nextp puintptr // 下次運作時的P
oldp puintptr // 在執行系統調用之前綁定的P
id int64 // m 的唯一id
mallocing int32
throwing int32
preemptoff string // 是否要保持 curg 始終在這個 m 上運作
locks int32
dying int32
profilehz int32
spinning bool // 為 true 時表示目前 m 處于自旋狀态,正在從其他線程偷工作
blocked bool // m 正阻塞在 note 上
newSigstack bool
printlock int8
incgo bool // 是否在執行 cgo 調用
freeWait uint32
fastrand [2]uint32
needextram bool
traceback uint8
// cgo 調用計數
ncgocall uint64
ncgo int32
cgoCallersUse uint32
cgoCallers *cgoCallers
// 沒有 goroutine 需要運作時,工作線程睡眠在這個 park 成員上,
// 其它線程通過這個 park 喚醒該工作線程
doesPark bool
park note
alllink *m // 記錄所有工作線程的連結清單
......
startingtrace bool
syscalltick uint32 // 執行過系統調用的次數
freelink *m
......
preemptGen uint32 // 完成的搶占信号數量
......
}
(1) M 的建立
M 是 runtime 中的一個對象,代表線程,每建立一個 M 對象同時會建立一個線程與 M 進行綁定,線程的建立是通過執行
clone()
系統調用建立出來的。runtime 中定義 M 的最大數量為 10000 個,使用者可以通過
debug.SetMaxThreads(n)
進行調整。
在以下兩種場景下會建立 M:
- 1、Golang 程式在啟動時會建立主線程,主線程是第一個 M 即 M0;
- 2、當有新的 G 建立或者有 G 從
進入_Gwaiting
且還有空閑的P,此時會調用_Grunning
,首先從全局隊列(startm()
)擷取一個 M 和空閑的 P 綁定執行 G,如果沒有空閑的 M 則會通過sched.midle
建立 M;newm()
(2) M 的銷毀
M 不會被銷毀,當找不到要運作的 G 或者綁定不到空閑的 P 時,會通過執行
stopm()
函數進入到睡眠狀态,在以下兩種情況下會執行
stopm()
函數進入到睡眠狀态:
- 1、當 M 綁定的 P 無可運作的 G 且無法從其它 P 竊取可運作的 G 時 M 會嘗試先進入自旋狀态 (
) ,隻有部分 M 會進入自旋狀态,處于自旋狀态的 M 數量最多為非空閑狀态的 P 數量的一半(spinning
),自旋狀态的 M 會從其他 P 竊取可執行的 G,如果 M 在自旋狀态未竊取到 G 或者未進入到自旋狀态則會直接進入到睡眠轉态;sched.nmspinning < (procs- sched.npidle)/2
- 2、當 M 關聯的 G 進入系統調用時,M 會主動和關聯的 P 解綁 ,當 M 關聯的 G 執行
函數退出系統調用時,M 會找一個空閑的 P 進行綁定,如果找不到空閑的 P 此時 M 會調用exitsyscall()
進入到睡眠狀态;stopm()
在
stopm()
函數中會将睡眠的 M 放到全局空閑隊列(
sched.midle
)中。
(3) M 的運作
M 需要與 P 關聯才能運作,并且 M 與 P 有親和性,比如在執行
entersyscall()
函數進入系統調用時,M 會主動與目前的 P 解綁,M 會将目前的 P 記錄到
m.oldp
中,在執行
exitsyscall()
函數退出系統調用時,M 會優先綁定
m.oldp
中的 P。
(4) M0 的作用以及與其他線程關聯的 M 差別?
M0 是一個全局變量,在
src/runtime/proc.go
定義,M0 不需要在堆上配置設定記憶體,其他 M 都是通過
new(m)
建立出來的對象,其記憶體是從堆上進行配置設定的,M0 負責執行初始化操作和啟動第一個 G,Golang 程式啟動時會首先啟動 M0,M0 和主線程進行了綁定,當 M0 啟動第一個 G 即 main goroutine 後功能就和其他的 M 一樣了 。
(5) 為什麼要限制 M 的數量?
Golang 在 1.2 版本時添加了對 M 數量的限制 (
runtime: limit number of operating system threads),M 預設的最大數量為 10000,在 1.17 版本中排程器初始化時在
schedinit()
函數中設定了預設值(
sched.maxmcount = 10000
)。
為什麼要限制 M 的數量?在重構排程器的文章中
Potential Further Improvements一節,Dmitry Vyukov 大佬已經提到過要限制 M 的數量了,在高并發或者反複會建立大量 goroutine 的場景中,需要更多的線程去執行 goroutine,線程過多時會耗盡系統資源或者觸發系統的限制導緻程式異常,核心在排程大量線程時也要消耗額外的資源,限制 M 的數量主要是防止程式不合理的使用。
Linux 上每個線程棧大小預設為 8M,如果建立 10000 個線程預設需要 78.125 G 記憶體,對普通程式來說記憶體使用量已經非常大了,此外,Linux 上下面這三個核心參數的大小也會影響建立線程的上限:
- /proc/sys/kernel/threads-max:表示系統支援的最大線程數;
- /proc/sys/kernel/pid_max:表示系統全局的 PID 号數值的限制,每一個程序或線程都有 ID,ID 的值超過這個數,程序或線程就會建立失敗;
- /proc/sys/vm/max_map_count:表示限制一個程序可以擁有的 VMA(虛拟記憶體區域)的數量;
(6) M 的狀态
通過 M 的建立和銷毀流程的分析,M 有三種狀态:運作、自旋、睡眠,這三種狀态之間的轉換如下所示:
3、G 的生命周期
G 的結構體資訊如下所示:
type g struct {
// 目前 Goroutine 的棧記憶體範圍
stack stack
stackguard0 uintptr
stackguard1 uintptr
_panic *_panic // 目前 g 中與 panic 相關的處理
_defer *_defer // 目前 g 中與 defer 相關的處理
m *m // 綁定的 m
// 存儲目前 Goroutine 排程相關的資料,上下方切換時會把目前資訊儲存到這裡
sched gobuf
......
param unsafe.Pointer // 喚醒G時傳入的參數
atomicstatus uint32 // 目前 G 的狀态
stackLock uint32
goid int64 // 目前 G 的 ID
schedlink guintptr
waitsince int64 // G 阻塞時長
waitreason waitReason // 阻塞原因
// 搶占标記
preempt bool
preemptStop bool
preemptShrink bool
asyncSafePoint bool
paniconfault bool
gcscandone bool
throwsplit bool
// 表示是否有未加鎖定的channel指向到了g 棧
activeStackChans bool
// 表示g 是放在chansend 還是 chanrecv,用于棧的收縮
parkingOnChan uint8
raceignore int8 // ignore race detection events
sysblocktraced bool // StartTrace has emitted EvGoInSyscall about this goroutine
tracking bool // whether we're tracking this G for sched latency statistics
trackingSeq uint8 // used to decide whether to track this G
runnableStamp int64 // timestamp of when the G last became runnable, only used when tracking
runnableTime int64 // the amount of time spent runnable, cleared when running, only used when tracking
sysexitticks int64 // cputicks when syscall has returned (for tracing)
traceseq uint64 // trace event sequencer
tracelastp puintptr // last P emitted an event for this goroutine
lockedm muintptr
sig uint32
writebuf []byte
sigcode0 uintptr
sigcode1 uintptr
sigpc uintptr
gopc uintptr // goroutine 目前運作函數的 PC 值
ancestors *[]ancestorInfo // ancestor information goroutine(s) that created this goroutine (only used if debug.tracebackancestors)
startpc uintptr // 觸發這個 goroutine 的函數的 PC 值
racectx uintptr
waiting *sudog // sudog structures this g is waiting on (that have a valid elem ptr); in lock order
cgoCtxt []uintptr // cgo traceback context
labels unsafe.Pointer // profiler labels
timer *timer // cached timer for time.Sleep
selectDone uint32 // are we participating in a select and did someone win the race?
// GC 時存儲目前 Goroutine 輔助标記的對象位元組數
gcAssistBytes int64
}
(1) G 的建立
在 Golang 程式啟動時,主線程會建立第一個 goroutine 來執行 main 函數,在 main 函數中如果使用者使用了 go 關鍵字會建立新的 goroutine ,在 goroutine 中使用者也可以使用 go 關鍵字繼續建立新的 goroutine。goroutine 的建立都是通過調用 golang runtime 中的
newproc()
函數來完成的。每個 goroutine 在建立時僅會配置設定 2K 大小,在 runtime 中沒有設定 goroutine 的數量上限。goroutine 的數量受系統資源的限制(CPU、記憶體、檔案描述符等)。如果 goroutine 中隻有簡單的邏輯,理論上起多少個 goroutine 都是沒有問題的,但 goroutine 裡面要是有建立網絡連接配接或打開檔案等操作,goroutine 過多可能會出現 too many files open 或 Resource temporarily unavailable 等報錯導緻程式執行異常。
建立的 G 會通過
runqput()
函數優先被放入到目前 G 關聯 P 的
runnext
隊列中,P 的
runnext
隊列中隻會儲存一個 G,如果
runnext
隊列中已經有 G,會用建立的 G 将其替換掉,然後将
runnext
中原來的 G 放到 P 的本地隊列即
runq
中,如果 P 的本地隊列滿了,則将 P 本地隊列一半的 G 移動到全局隊列
sched.runq
中。此處将建立的 G 首先移動到 P 的
runnext
中主要是為了提高性能,
runnext
是 P 完全私有的隊列,如果将 G 放在 P 本地隊列
runq
中,
runq
隊列中的 G 可能因其他 M 的竊取發生了變化,每一次從 P 本地隊列擷取 G 時都需要執行
atomic.LoadAcq
和
atomic.CasRel
原子操作,這會帶來額外的開銷。
(2) G 的銷毀
G 在退出時會執行
goexit()
函數,G 的狀态會從
_Grunning
轉換為
_Gdead
,但 G 對象并不會被直接釋放 ,而是會通過
gfput()
被放入到所關聯 P 本地或者全局的閑置清單
gFree
中以便複用,優先放入到 P 本地隊列中,如果 P 本地隊列中
gFree
超過 64 個,僅會在 P 本地隊列中儲存 32 個,把超過的 G 都放入到全局閑置隊列
sched.gFree
中。
(3) G 的 運作
G 與 M 綁定才能運作,而 M 需要與 P 綁定才能運作,是以理論上同一時間運作 G 的數量等于 P 的數量,M 不保留 G 的狀态,G 會将狀态保留在其
gobuf
字段,是以 G 可以跨 M 進行排程。M 在找到需要運作的 G 後,會通過彙編函數
gogo()
從 g0 棧切換到使用者 G 的棧運作。
(4) G 有哪些狀态?
G 的狀态在
src/runtime/runtime2.go
檔案中定義了,主要分為三種,一個是 goroutine 正常運作時的幾個狀态,然後是與 GC 有關的狀态的,其餘幾個狀态是未使用的。
每種轉态的作用以及狀态之間的轉換關系如下所示:
| 剛剛被建立并且還沒有被初始化 |
| 沒有執行代碼,沒有棧的所有權,存儲在運作隊列中 |
| 可以執行代碼,擁有棧的所有權,已經綁定了 M 和 P |
| 正在執行系統調用 |
| 由于運作時而被阻塞,沒有執行使用者代碼并且不在運作隊列上 |
| 運作完成處于退出狀态 |
| 棧正在被拷貝 |
| 由于搶占而被阻塞,等待喚醒 |
| GC 正在掃描棧空間 |
4、g0 的作用
type m struct {
g0 *g // goroutine with scheduling stack
......
}
在 runtime 中有兩種 g0,一個是 m0 關聯的 g0,另一種是其他 m 關聯的 g0,m0 關聯的 g0 是以全局變量的方式定義的,其記憶體空間是在系統的棧上進行配置設定的,大小為 64K - 104 位元組,其他 m 關聯的 g0 是在堆上配置設定的棧,預設為 8K。
src/runtime/proc.go#1879
if iscgo || mStackIsSystemAllocated() {
mp.g0 = malg(-1)
} else {
// sys.StackGuardMultiplier 在 linux 系統上值為 1
mp.g0 = malg(8192 * sys.StackGuardMultiplier)
}
每次啟動一個 M 時,建立的第一個 goroutine 就是 g0,每個 M 都會有自己的 g0,g0 主要用來記錄工作線程使用的棧資訊,僅用于負責排程,在執行排程代碼時需要使用這個棧。執行使用者 goroutine 代碼時,使用使用者 goroutine 的棧,排程時會發生棧的切換。
在 runtime 代碼中很多函數在執行時都會通過
systemstack()
函數封裝調用,
systemstack()
函數的作用是切換到 g0 棧,然後執行對應的函數最後再切換回原來的棧并傳回,為什麼這些代碼需要在 g0 棧上運作?原則上隻要某函數有
nosplit
這個系統注解就需要在 g0 棧上執行,因為加了
nosplit
編譯器在編譯時不會在函數前面插入檢查棧溢出的代碼,這些函數在執行時有可能會導緻棧溢出,而 g0 棧比較大,在編譯時如果對runtime中每個函數都做棧溢出檢查會影響效率,是以才會切到 g0 棧。
總結
本文主要分析了 Golang GPM 的模型,在閱讀 runtime 代碼的過程中發現代碼中有很多細節需要花大量時間分析,文中僅對其大架構做了一些簡單的說明,也有部分細節順便被帶入,在後面的文章中,會對許多細節再次進行分析。
參考:
https://docs.google.com/document/d/1TTj4T2JO42uD5ID9e89oa0sLKhJYD0Y_kqxDv3I3XMw/edit(Scalable Go Scheduler Design Doc)
https://docs.google.com/document/d/1flyIICFZV_kMfypiaghcZx0BLIC-aIooSALo1S6ZJIY/edit(dev.cc branch plan)
https://learnku.com/articles/41728 https://yizhi.ren/2019/06/03/goscheduler/ https://colobu.com/2020/12/20/threads-in-go-runtime/ https://golang.design/under-the-hood/zh-cn/part2runtime/ch06sched/mpg/ https://github.com/golang/go/wiki/DesignDocuments https://github.com/golang/proposal http://www1.cs.columbia.edu/~aho/cs6998/reports/12-12-11_DeshpandeSponslerWeiss_GO.pdf https://zhuanlan.zhihu.com/p/339837580 https://www.jianshu.com/p/67b0cb8e8bdc https://golang.design/go-questions/sched/gpm/ https://github.com/MrYueQ/go-under-the-hood/blob/master/content/4-sched/exec.md https://hjlarry.github.io/docs/go/goroutine/ https://www.jianshu.com/p/1a50330adf1b