最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

Go語言中GMP調(diào)度模型詳解

 更新時(shí)間:2026年05月12日 09:10:39   作者:~|Bernard|  
文章解釋了Go語言的GMP模型,介紹了G、M、P三者的定義、協(xié)作關(guān)系和調(diào)度策略,本文給大家介紹了Go語言中GMP調(diào)度模型,感興趣的朋友一起看看吧

如果說 Goroutine 是 Go 并發(fā)的"工人",Channel 是"傳送帶",那 GMP 就是整個(gè)工廠的調(diào)度指揮中心。

一、Go 語言中的 GMP 模型是什么?

1.1 三個(gè)字母的本質(zhì)

GMP 是 Go Runtime 設(shè)計(jì)的三級調(diào)度抽象

字母全稱本質(zhì)生活比喻
GGoroutine用戶任務(wù),待執(zhí)行的函數(shù)工人(帶著圖紙和工具,等待派活)
MMachineOS 線程(內(nèi)核線程),真正跑代碼的執(zhí)行引擎機(jī)床/工位(有電、有馬達(dá),能干活)
PProcessor邏輯處理器,包含調(diào)度上下文和本地隊(duì)列車間主任(手里有本地任務(wù)清單、工具箱、材料柜)

1.2 三者如何協(xié)作?(核心關(guān)系)

就是這個(gè)模型讓Go能在少量線程上調(diào)度海量goroutine,是Go?并發(fā)的基礎(chǔ)。

鐵律:

  • G 想執(zhí)行,必須先找到 P。沒有 P 的 G 只能去全局隊(duì)列排隊(duì)。
  • P 想干活,必須先綁定 M。P 是邏輯概念,不能自己跑代碼,必須寄生在 M 上。
  • M 可以沒有 P(比如執(zhí)行系統(tǒng)調(diào)用時(shí)),但此時(shí)它不能執(zhí)行用戶 G。

1.3 為什么需要三層?不是兩層就行了嗎?

P 是性能關(guān)鍵。

如果沒有 P,直接讓 G 和 M 綁定:

  • M1 綁定了 G1,G1 去 Channel 阻塞了 → M1 這個(gè)內(nèi)核線程也跟著阻塞。
  • 操作系統(tǒng)調(diào)度器會(huì)把 M1 切走,上下文切換開銷巨大。
  • 所有 G 競爭一個(gè)全局隊(duì)列,鎖爭用慘烈。

有了 P 之后:

  • G1 阻塞在 Channel,只是 G1 被掛起,M 可以立刻切換到 g0(后面講),然后綁定 P 去執(zhí)行別的 G。
  • P 的本地隊(duì)列讓大部分調(diào)度無需加鎖(無鎖環(huán)形隊(duì)列)。

二、什么是 Go Scheduler?

2.1 本質(zhì)定義

Go Scheduler 是 Go Runtime 內(nèi)部實(shí)現(xiàn)的一個(gè)用戶態(tài)線程調(diào)度器(User-space Scheduler),運(yùn)行在用戶空間,與操作系統(tǒng)內(nèi)核調(diào)度器(如 Linux CFS)是上下級關(guān)系

它不是操作系統(tǒng)的某個(gè)組件,而是編譯進(jìn)你程序里的 runtime 包代碼(主要在 runtime/proc.goruntime/runtime2.go 中)。

2.2 它到底在管什么?

它只管理一件事:哪個(gè) G(Goroutine)應(yīng)該在哪個(gè) M(OS 線程)上執(zhí)行。

操作系統(tǒng)內(nèi)核調(diào)度器管理的是進(jìn)程/線程(M),它根本不知道 G 的存在。Go Scheduler 夾在操作系統(tǒng)和你的業(yè)務(wù)代碼之間,扮演"中間商"

2.3 為什么需要它?直接用操作系統(tǒng)線程不行嗎?

核心原因:OS 線程太重,Goroutine 太輕。

對比項(xiàng)OS 線程(Linux pthread)Goroutine
棧大小默認(rèn) 1~8 MB(固定或分頁增長)默認(rèn) 2 KB(可動(dòng)態(tài)增長)
上下文切換1~2 微秒(進(jìn)內(nèi)核,保存/恢復(fù)大量寄存器,刷新 TLB)~200 納秒(用戶態(tài),只改幾個(gè)寄存器)
創(chuàng)建銷毀開銷大(需內(nèi)核參與,分配大量資源)極?。ㄓ脩魬B(tài)分配,約 ~2KB 內(nèi)存)
單機(jī)并發(fā)量通常幾千個(gè)就是極限輕松幾十萬甚至上百萬

如果沒有 Go Scheduler,一個(gè) Goroutine 綁定一個(gè) OS 線程:

  • 100 萬個(gè) Goroutine = 100 萬個(gè)線程。
  • 操作系統(tǒng)直接崩潰,內(nèi)存不夠,調(diào)度器本身也卡死。

Go Scheduler 通過 M:N 模型(少量 M 承載大量 G),把 Goroutine 的調(diào)度成本壓到極低。

2.4 Scheduler 的核心入口函數(shù)

Go 程序里每個(gè) M 都在執(zhí)行一個(gè)永不退出的調(diào)度循環(huán)

// runtime/proc.go 中的核心函數(shù)
func schedule() {
    // 1. 找下一個(gè)可運(yùn)行的 G
    gp := findRunnable()
    // 2. 執(zhí)行它
    execute(gp)
    // 3. G 結(jié)束后,回到這里,繼續(xù)循環(huán)
}

這個(gè) schedule() 函數(shù)就是 Go Scheduler 的心臟。你的程序里每個(gè) M(除了極少數(shù)特殊情況)都在死循環(huán)地調(diào)用它。

三、Go 調(diào)度策略是什么?

3.1 大前提:M:N 調(diào)度 + 兩級隊(duì)列

Go 的調(diào)度器采用的是 M:N 模型

  • M 個(gè) OS 線程,調(diào)度 N 個(gè) Goroutine(N >> M)。
  • 調(diào)度隊(duì)列分兩級:P 的本地?zé)o鎖隊(duì)列(Local Runq)+ 全局隊(duì)列(Global Runq)。

3.2 搶占策略的演進(jìn)

Go 的調(diào)度不是純粹的協(xié)作式,也不是純粹的操作系統(tǒng)搶占式,而是"以協(xié)作為主,強(qiáng)制搶為輔"的混合策略。=

① Go 1.14 之前:協(xié)作式搶占(Cooperative Preemption)

原理: 編譯器在幾乎所有函數(shù)調(diào)用的入口處(函數(shù)序言,F(xiàn)unction Prologue),插入一段檢查代碼。這段代碼會(huì)檢查當(dāng)前 G 是否被標(biāo)記為需要讓出 CPU。

源碼層面的實(shí)現(xiàn): 每個(gè) G 結(jié)構(gòu)體里有一個(gè)字段叫 stackguard0。當(dāng) sysmon 認(rèn)為某個(gè) G 運(yùn)行太久了,就把它的 stackguard0 設(shè)置為一個(gè)特殊值 stackpreempt。

當(dāng) G 執(zhí)行到下一個(gè)函數(shù)調(diào)用時(shí),會(huì)檢查??臻g:

// 偽代碼,示意函數(shù)序言的檢查邏輯
if sp < stackguard0 {  // 正常是檢查棧溢出
    if stackguard0 == stackpreempt {
        // 不是棧溢出,是被要求搶占了!
        goschedImpl()  // 主動(dòng)讓出 CPU
    }
}

致命缺陷: 如果某個(gè) Goroutine 寫了一個(gè)純死循環(huán),且循環(huán)體里沒有任何函數(shù)調(diào)用

for {}

那么它永遠(yuǎn)不會(huì)走到函數(shù)序言的檢查點(diǎn)preempt 標(biāo)志永遠(yuǎn)得不到檢查。這個(gè) G 會(huì)永久霸占所在的 M,同一個(gè) P 本地隊(duì)列里的其他 G 全部餓死。

② Go 1.14 之后:基于信號的異步搶占(Asynchronous Preemption)

Go 1.14 引入了新機(jī)制,解決了"無函數(shù)調(diào)用的死循環(huán)"問題。

完整流程:

Step 1:Sysmon 檢測

  • sysmon 是一個(gè)特殊的 M(由 Runtime 啟動(dòng),不綁定 P),它死循環(huán)運(yùn)行。
  • 它定期檢查每個(gè) P 的 sysmontick 記錄。
  • 如果發(fā)現(xiàn)某個(gè) G 在同一個(gè) M 上運(yùn)行超過 10ms,判定為"需要搶占"。

Step 2:發(fā)送信號

  • sysmon 向目標(biāo) M 發(fā)送一個(gè) Unix 信號 SIGURG(緊急信號,用戶程序通常不用)。
  • 注意:不是直接操作 G,而是給 M 發(fā)信號。

Step 3:M 的信號處理

  • 每個(gè) M 內(nèi)部都有一個(gè)專門的信號處理 Goroutine,叫做 gsignal。
  • M 收到 SIGURG 后,由 gsignal 接手處理(在信號上下文執(zhí)行)。
  • gsignal 會(huì)設(shè)置目標(biāo) G 的搶占標(biāo)志,并記錄"異步搶占請求"。

Step 4:安全點(diǎn)搶占

  • 信號處理程序不會(huì)直接打斷正在運(yùn)行的 G(那樣太危險(xiǎn),可能正在修改共享狀態(tài))。
  • 它會(huì)在 G 到達(dá)下一個(gè)安全點(diǎn)(Safe Point)時(shí)觸發(fā)搶占。安全點(diǎn)包括:
    • 函數(shù)調(diào)用入口
    • 循環(huán)的 backedge(循環(huán)邊界)
    • 某些特定的指令序列
  • 在安全點(diǎn),Runtime 檢查搶占標(biāo)志,然后執(zhí)行和之前類似的 goschedImpl(),把 G 放回隊(duì)列。

關(guān)鍵結(jié)論:

  • 1.14 之前:只有函數(shù)調(diào)用時(shí)才能被搶占。
  • 1.14 之后:即使無函數(shù)調(diào)用,信號機(jī)制也能強(qiáng)制你在安全點(diǎn)停下。但注意,它仍然不是"隨時(shí)打斷",而是"在安全點(diǎn)檢查并打斷"。

四、發(fā)生調(diào)度的時(shí)機(jī)有哪些?

調(diào)度不是隨時(shí)發(fā)生的,而是在特定時(shí)機(jī)觸發(fā)。分為主動(dòng)、被動(dòng)、搶占三類:

4.1 主動(dòng)調(diào)度(Goroutine 自己讓出)

場景函數(shù)說明
顯式讓出runtime.Gosched()G 主動(dòng)說"我先歇會(huì)兒"
parkgopark()Channel 阻塞、sleep、wait 等,G 掛起
結(jié)束G 的函數(shù) returnG 執(zhí)行完了,M 必須找新 G

4.2 被動(dòng)調(diào)度(Goroutine 被迫讓出)

場景說明
系統(tǒng)調(diào)用G 調(diào)用 read/write 等,M 進(jìn)入內(nèi)核態(tài),G 和 M 解綁,P 找新 M
Channel 阻塞無緩沖 Channel 沒配對,G 掛到隊(duì)列,M 切換
鎖阻塞sync.Mutex 拿不到,G 進(jìn)等待隊(duì)列
GC觸發(fā) GC 時(shí),所有 G 必須暫停(STW),調(diào)度器介入
棧增長G 的棧不夠用了,切換到 g0 分配新棧

4.3 搶占式調(diào)度(Go 1.14+ 的關(guān)鍵改進(jìn))

以前 Go 是協(xié)作式調(diào)度,如果 G 里寫了個(gè)死循環(huán)且不調(diào)用任何函數(shù),它會(huì)永遠(yuǎn)占著 CPU。

Go 1.14 引入了基于信號的搶占

  • 系統(tǒng)監(jiān)控線程(Sysmon)每 10ms 檢查一次。
  • 發(fā)現(xiàn)某個(gè) G 運(yùn)行太久,就發(fā)信號(SIGURG)給對應(yīng)的 M。
  • M 收到信號后,在函數(shù)調(diào)用序言(prologue)處檢查搶占標(biāo)志,主動(dòng)讓出。

五、M 尋找可運(yùn)行 G 的過程是怎樣的?

這是 Go Scheduler 最核心的算法,我必須嚴(yán)格按照 runtime/proc.gofindrunnable() 的源碼邏輯,給你分步驟拆解

總覽

當(dāng) M 上的當(dāng)前 G 執(zhí)行完畢(或阻塞)后,M 會(huì)調(diào)用 schedule(),進(jìn)而調(diào)用 findrunnable() 來找下一個(gè)活干。

Step 1:檢查 P 的 runnext(最快路徑)

if gp := pp.runnext; gp != nil {
    pp.runnext = nil
    return gp
}

每個(gè) P 有一個(gè)單緩沖槽位runnext。如果某個(gè) G 被標(biāo)記為"下一個(gè)優(yōu)先執(zhí)行"(比如剛被喚醒的高優(yōu)先級 G),直接拿走。無鎖,最快。

Step 2:從 P 的本地隊(duì)列?。╮unqget)

if gp, inheritTime := runqget(_p_); gp != nil {
    return gp, inheritTime
}

P 的 runq 是一個(gè)無鎖環(huán)形數(shù)組隊(duì)列(長度 256)。M 優(yōu)先從這里取 G。這也是無鎖操作,性能極高。

Step 3:從全局隊(duì)列取(globrunqget)

if gp := globrunqget(_p_, max); gp != nil {
    return gp
}

本地隊(duì)列空了。M 去全局隊(duì)列 sched.runq 取一批 G。

  • 取多少個(gè)?n = min(len(globalRunq), max),通常一次取 1 個(gè)或一批(具體算法會(huì)取一半,但上限約 128 個(gè))。
  • 為什么取一批而不是一個(gè)?減少全局鎖競爭。
  • 這批 G 被放到 P 的本地隊(duì)列,然后執(zhí)行第一個(gè)。

Step 4:檢查 Netpoller(網(wǎng)絡(luò)輪詢器)

if gp := netpoll(0); gp != nil {
    return gp
}

看看有沒有因?yàn)榫W(wǎng)絡(luò) I/O 阻塞、但底層 fd 已經(jīng)就緒的 G(比如 socket 收到數(shù)據(jù)了)。這是一個(gè)非阻塞調(diào)用。

Step 5:Work Stealing(工作竊?。?/h3>
// 嘗試 4 輪
for i := 0; i < 4; i++ {
    // 隨機(jī)選擇其他 P
    for _, p2 := range stealOrder {
        if gp := runqsteal(_p_, p2); gp != nil {
            return gp
        }
    }
}
  • M 隨機(jī)挑選其他 P,嘗試從它們的本地隊(duì)列尾部偷一半任務(wù)過來。
  • 偷取是批量的:把 victim P 隊(duì)列里后半部分的 G 一次性搬到當(dāng)前 P 的隊(duì)列。
  • 嘗試 4 輪還偷不到,放棄。

Step 6:再次檢查全局隊(duì)列和 Netpoller

偷了一圈回來,可能全局隊(duì)列有新任務(wù)了,或者網(wǎng)絡(luò)包到了。再檢查一次。

Step 7:GC 后臺(tái)任務(wù)

if gp := gcBgMarkWorker(); gp != nil {
    return gp
}

實(shí)在沒用戶任務(wù)了,M 可以幫 GC 做后臺(tái)標(biāo)記工作(并發(fā) GC 的一部分)。

Step 8:真的沒有了,M 進(jìn)入休眠(stopm)

stopm()
  • M 和 P 解綁(P 被放回全局 pidle 空閑列表)。
  • M 自己被放入全局 midle 空閑列表。
  • 調(diào)用操作系統(tǒng)的線程休眠原語(如 futex / WaitForSingleObject),進(jìn)入睡眠狀態(tài)。
  • 等待將來有任務(wù)時(shí),被 startm()wakep() 叫醒。

六、GMP 能不能去掉 P 層?去掉會(huì)怎么樣?

6.1 結(jié)論:絕對不能去掉

如果你把 P 層抽掉,只剩下 G 和 M 兩層,Go 的 Runtime 會(huì)在調(diào)度性能內(nèi)存分配性能上同時(shí)崩塌。P 層存在的意義,遠(yuǎn)不止"一個(gè)中間層"這么簡單。

6.2 原因一:本地?zé)o鎖隊(duì)列(Local Runq)

這是 P 層最核心的功能。

有 P 時(shí)的場景:

  • 每個(gè) P 有一個(gè) runq[256] 的環(huán)形數(shù)組。
  • M 從 P 的本地隊(duì)列取 G 時(shí),不需要任何鎖(無鎖環(huán)形隊(duì)列,通過原子操作實(shí)現(xiàn))。
  • 4 個(gè) P 就有 4 個(gè)獨(dú)立隊(duì)列,4 個(gè) M 可以同時(shí)取 G,互不干擾。

去掉 P 后的場景:

  • 所有 G 只能擠在一個(gè)全局隊(duì)列里。
  • 任何 M 想取 G,都必須先搶一把全局大鎖。
  • 4 個(gè) M 同時(shí)取 G → 4 個(gè)線程搶同一把鎖 → 緩存一致性流量爆炸 → CPU 大量時(shí)間花在自旋等待上。
  • 實(shí)測:高并發(fā)下,調(diào)度器本身會(huì)吃掉 30%~50% 的 CPU 時(shí)間。

6.3 原因二:mcache(內(nèi)存分配無鎖)

這是很多人忽略的一點(diǎn)。P 層不僅管調(diào)度,還管內(nèi)存分配。

Go 的內(nèi)存分配器采用 TCMalloc 模型,分為三級:

G ──? mcache(P 本地)──? mcentral(全局,按 span class 分)──? mheap(全局,向 OS 申請)

mcache 是什么?

  • 每個(gè) P 自帶一個(gè) mcache,里面緩存了各種規(guī)格(size class)的內(nèi)存塊(span)。
  • 當(dāng) G 申請小對象(< 32KB)時(shí),直接從 P 的 mcache 拿,不需要加鎖
  • 只有當(dāng) mcache 空了,才會(huì)去全局的 mcentral 補(bǔ)充(這時(shí)才需要鎖)。

去掉 P 后的場景:

  • 所有 G 申請內(nèi)存都直接走全局 mcentralmheap。
  • 100 萬個(gè) G 同時(shí)搶內(nèi)存分配鎖 → 程序直接卡死。
  • 有 P 時(shí),99% 的內(nèi)存分配是無鎖的。

6.4 原因三:調(diào)度上下文緩存

P 保存了一個(gè) G 運(yùn)行所需的完整上下文環(huán)境

  • mcache:內(nèi)存分配緩存。
  • pctrace:性能分析采樣狀態(tài)。
  • defer pool:defer 結(jié)構(gòu)體緩存池,減少重復(fù)分配。
  • sudog cache:Channel 操作時(shí) sudog 對象的緩存池。
  • writebuf:GC 寫屏障緩沖區(qū)。

這意味著: 當(dāng) G1 在 P 上阻塞后,P 可以立刻把 G1 的上下文凍結(jié),然后執(zhí)行 G2。G1 恢復(fù)時(shí),P 上的緩存還在,無需重建。

去掉 P 后: 每次切換 G,都要重新初始化內(nèi)存分配環(huán)境、重新分配 defer/sudog 緩存,開銷巨大。

七、P 和 M 在什么時(shí)候會(huì)被創(chuàng)建?

7.1 P 的創(chuàng)建時(shí)機(jī)

P 是程序啟動(dòng)時(shí)一次性創(chuàng)建的。

func main() {
 ? ?runtime.GOMAXPROCS(4) ?// 默認(rèn)等于 CPU 核心數(shù)
}

具體流程:

  • Go 程序啟動(dòng),執(zhí)行 runtime.rt0_go。
  • 調(diào)用 schedinit(),其中讀取環(huán)境變量 $GOMAXPROCS 或調(diào)用 runtime.GOMAXPROCS(n)。
  • 調(diào)用 procresize(n),創(chuàng)建 n 個(gè) P。
  • 這些 P 被放入全局 allp 數(shù)組,同時(shí)大部分被放入 空閑池 pidle

P 的數(shù)量是固定的嗎?

  • 運(yùn)行時(shí)可以動(dòng)態(tài)調(diào)整(runtime.GOMAXPROCS(newN)),但極少這么做。
  • 調(diào)整時(shí),多余的 P 會(huì)被剝離(上面的 G 轉(zhuǎn)移到其他 P),然后放入 pidle;不足的 P 從 pidle 取出或新建。

pidle 是什么?

  • pidle 是全局的 P 空閑鏈表(singly-linked list)。
  • 當(dāng) M 進(jìn)入休眠(stopm)時(shí),它綁定的 P 會(huì)被解綁,放入 pidle。
  • 當(dāng)有新任務(wù)時(shí),wakep()startm()pidle 取一個(gè) P,綁定到 M 上。

7.2 M 的創(chuàng)建時(shí)機(jī)

M 是 OS 線程,創(chuàng)建成本較高,Go 會(huì)盡量復(fù)用。M 是動(dòng)態(tài)創(chuàng)建的。

創(chuàng)建 M 的入口是 newm(),觸發(fā)場景:

場景 1:程序啟動(dòng)時(shí)

創(chuàng)建第一個(gè) M,即 m0(主線程)。

場景 2:有 G 要執(zhí)行,但沒有空閑 M

// 當(dāng)需要執(zhí)行 G 時(shí),調(diào)用 startm()
func startm(_p_ *p, spinning bool) {
 ? ?// 1. 先嘗試從 midle(M 空閑列表)拿一個(gè)休眠的 M
 ? ?mp := mget()
 ? ?if mp == nil {
 ? ? ? ?// 2. 沒有空閑 M,新建一個(gè)!
 ? ? ? ?newm(fn, _p_, spinning)
 ? ? ? ?return
 ?  }
 ? ?// ...綁定 P,喚醒 M
}

場景 3:G 進(jìn)入系統(tǒng)調(diào)用,當(dāng)前 M 阻塞

當(dāng) G 執(zhí)行 read() / write() 等系統(tǒng)調(diào)用時(shí):

  • 當(dāng)前 M 進(jìn)入內(nèi)核態(tài),可能長時(shí)間阻塞。
  • P 和 M 解綁(P 不能跟著 M 一起睡)。
  • P 需要找新的 M 來繼續(xù)執(zhí)行其他 G。
  • 如果 midle 里沒有空閑 M,就調(diào)用 newm() 新建一個(gè)。

場景 4:GC 并行標(biāo)記需要

GC 的并發(fā)標(biāo)記階段可能需要額外的 M 并行工作。

midle 是什么?

  • midle 是全局的 M 空閑鏈表
  • M 沒活干時(shí)(stopm),不會(huì)被操作系統(tǒng)銷毀,而是放入 midle 睡覺。
  • 下次需要 M 時(shí),優(yōu)先從 midle 喚醒,避免頻繁創(chuàng)建/銷毀 OS 線程。

八、m0 是什么?有什么用?

8.1 m0 的定義

m0 是 Go 程序啟動(dòng)時(shí),由操作系統(tǒng)創(chuàng)建的第一個(gè) OS 線程(主線程)。 它是 Go Runtime 的"盤古",一切從這里開始。

8.2 m0 的特殊身世

普通 M 是 Go Runtime 自己調(diào)用操作系統(tǒng) API(如 clone / CreateThread)創(chuàng)建的,但 m0 不是——它是操作系統(tǒng)啟動(dòng)你的程序時(shí),就已經(jīng)存在的那條線程。

操作系統(tǒng)加載可執(zhí)行文件
 ?  │
 ?  ▼
創(chuàng)建進(jìn)程 + 主線程
 ?  │
 ?  ▼
進(jìn)入 _rt0_amd64_linux(或?qū)?yīng)平臺(tái)的入口)
 ?  │
 ?  ▼
這條主線程,就是 m0

8.3 m0 的不可替代職責(zé)

階段m0 做什么
Runtime 初始化執(zhí)行 runtime.rt0_go,設(shè)置棧、初始化內(nèi)存分配器、初始化調(diào)度器
創(chuàng)建 G0 和 main goroutine創(chuàng)建第一個(gè) G(不是 g0,是執(zhí)行 main.main 的那個(gè) G)
進(jìn)入調(diào)度循環(huán)m0 也調(diào)用 mstart()schedule(),和普通 M 一樣干活
兜底如果所有其他 M 都阻塞在系統(tǒng)調(diào)用里,至少 m0 還在運(yùn)行,保證程序不死

8.4 m0 與普通 M 的區(qū)別

特性m0普通 M
創(chuàng)建者操作系統(tǒng)Go Runtime (newm)
g0 棧使用操作系統(tǒng)分配的初始棧使用 Runtime 在堆上分配的棧
生命周期與進(jìn)程同生共死可以休眠復(fù)用,理論上可銷毀(實(shí)際很少)
數(shù)量全局只有 1 個(gè)可以有多個(gè)

九、g0 是一個(gè)怎樣的協(xié)程?有什么用?

9.1 g0 的定義

每個(gè) M 都有一個(gè)專屬的 g0,它是一個(gè)"系統(tǒng) Goroutine",不執(zhí)行用戶代碼,只執(zhí)行 Runtime 代碼。

9.2 g0 與普通 G 的本質(zhì)區(qū)別

特性普通 Gg0
2KB 起步,動(dòng)態(tài)增長(通常幾 KB 到幾 MB)較大(通常 8KB+),固定大小,系統(tǒng)棧
執(zhí)行內(nèi)容用戶寫的函數(shù)Runtime 內(nèi)部函數(shù)(schedule、GC、syscall 封裝)
調(diào)度行為被調(diào)度(被動(dòng)等待 M 執(zhí)行)主動(dòng)調(diào)度別人(執(zhí)行 schedule() 決定下一個(gè)跑誰)
數(shù)量用戶創(chuàng)建,無數(shù)多個(gè)每個(gè) M 嚴(yán)格只有 1 個(gè)

9.3 g0 的三大核心作用

作用 1:調(diào)度中轉(zhuǎn)站(最重要的作用)

當(dāng) M 上的普通 G1 需要讓出 CPU 時(shí),必須切換到 g0,由 g0 執(zhí)行調(diào)度邏輯。

G1 正在執(zhí)行用戶代碼
 ?  │
 ?  ▼
G1 阻塞(Channel / Sleep / 系統(tǒng)調(diào)用)
 ?  │
 ?  ▼
保存 G1 的上下文(SP, PC, BP)到 G1.gobuf
 ?  │
 ?  ▼
【切換到 g0 棧】← 關(guān)鍵!
 ?  │
 ?  ▼
g0 執(zhí)行 schedule() → findrunnable() → 找到 G2
 ?  │
 ?  ▼
恢復(fù) G2 的上下文
 ?  │
 ?  ▼
【從 g0 切換回 G2】
 ?  │
 ?  ▼
G2 開始執(zhí)行

為什么必須切到 g0? 因?yàn)?schedule() 本身是 Runtime 函數(shù),它如果跑在普通 G 的棧上,可能會(huì)污染用戶棧(比如占用太多??臻g、修改棧布局)。g0 有獨(dú)立的、足夠大的系統(tǒng)棧,可以安全地執(zhí)行復(fù)雜的調(diào)度邏輯。

作用 2:系統(tǒng)調(diào)用的代理

普通 G 執(zhí)行 read() / write() 時(shí):

  • 切換到 g0 棧。
  • g0 調(diào)用 entersyscall(),保存狀態(tài),把 G 標(biāo)記為 _Gsyscall。
  • g0 執(zhí)行真正的系統(tǒng)調(diào)用。
  • 系統(tǒng)調(diào)用返回后,g0 調(diào)用 exitsyscall(),嘗試重新獲取 P,恢復(fù)用戶 G。

作用 3:棧增長管理

當(dāng)普通 G 的棧不夠用時(shí),會(huì)觸發(fā) morestack。這個(gè)函數(shù)運(yùn)行在 g0 棧上,負(fù)責(zé)分配更大的??臻g,拷貝舊棧數(shù)據(jù),然后切回用戶 G。

十、Go 棧和用戶棧是如何進(jìn)行切換的?

10.1 兩種棧的明確區(qū)分

歸屬用途大小
用戶棧(G 棧)每個(gè)普通 G 獨(dú)有執(zhí)行用戶函數(shù)、局部變量、defer、panic recover2KB 起步,可動(dòng)態(tài)增長
系統(tǒng)棧(g0 棧)每個(gè) M 的 g0 獨(dú)有執(zhí)行 Runtime 代碼、調(diào)度、系統(tǒng)調(diào)用較大,固定

10.2 切換的核心數(shù)據(jù)結(jié)構(gòu):gobuf

Go 在 G 結(jié)構(gòu)體里保存了切換所需的全部寄存器狀態(tài):

type gobuf struct {
 ? ?sp ? uintptr ?// Stack Pointer:棧頂指針
 ? ?pc ? uintptr ?// Program Counter:下一條要執(zhí)行的指令地址
 ? ?g ? ?uintptr ?// 指向當(dāng)前 G 的指針(恢復(fù)時(shí)知道切到哪個(gè) G)
 ? ?bp ? uintptr ?// Frame Pointer:幀指針/基址指針
 ? ?ret ?uintptr ?// 返回值(某些架構(gòu)用)
 ? ?lr ? uintptr ?// Link Register:返回地址(ARM 等架構(gòu)用)
}

10.3 完整切換過程(以 G1 阻塞、切換到 G2 為例)

階段 A:保存 G1 的現(xiàn)場(G1 → g0)

發(fā)生時(shí)機(jī): G1 執(zhí)行到 Channel 發(fā)送/接收、Sleep、鎖阻塞等。

; 偽匯編
; 1. 把 G1 的寄存器保存到 G1.sched(gobuf)
MOVQ SP, G1.sched.sp ? ? ?; 保存棧指針
MOVQ PC, G1.sched.pc ? ? ?; 保存程序計(jì)數(shù)器(當(dāng)前指令地址)
MOVQ BP, G1.sched.bp ? ? ?; 保存幀指針
MOVQ $G1, G1.sched.g ? ? ?; 保存 G 指針
?
; 2. 修改 G1 狀態(tài)
MOVQ $_Gwaiting, G1.status ?; G1 變成等待狀態(tài)
?
; 3. 切換到 g0 棧
MOVQ g0.sched.sp, SP ? ? ? ?; SP 切到 g0 的棧頂
MOVQ g0, g ? ? ? ? ? ? ? ? ?; 當(dāng)前 G 寄存器指向 g0
?
; 4. 調(diào)用 runtime.schedule()
CALL runtime.schedule(SB)

階段 B:g0 執(zhí)行調(diào)度

func schedule() {
 ? ?gp := findRunnable() ?// 找到 G2
 ? ?execute(gp) ? ? ? ? ? // 準(zhǔn)備執(zhí)行 G2
}

階段 C:恢復(fù) G2 的現(xiàn)場(g0 → G2)

; 偽匯編
; 1. 修改 G2 狀態(tài)
MOVQ $_Grunning, G2.status
?
; 2. 從 G2.sched 恢復(fù)寄存器
MOVQ G2.sched.sp, SP ? ? ?; 恢復(fù)棧指針
MOVQ G2.sched.pc, PC ? ? ?; 恢復(fù)程序計(jì)數(shù)器(下一條指令)
MOVQ G2.sched.bp, BP ? ? ?; 恢復(fù)幀指針
MOVQ G2.sched.g, g ? ? ? ?; 當(dāng)前 G 寄存器指向 G2
?
; 3. 跳轉(zhuǎn)到 G2 的 PC 繼續(xù)執(zhí)行
JMP G2.sched.pc

關(guān)鍵點(diǎn): 這不是操作系統(tǒng)級別的線程切換(不需要進(jìn)內(nèi)核),只是修改了幾個(gè) CPU 寄存器(SP、PC、BP、g),所以速度極快(約 200 納秒)。

10.4 系統(tǒng)調(diào)用時(shí)的特殊切換

當(dāng) G1 執(zhí)行 read() 時(shí),切換更復(fù)雜:

G1 用戶棧
 ?  │
 ?  ▼
調(diào)用 syscall.Read
 ?  │
 ?  ▼
【切換到 g0 ?!?
 ?  │
 ?  ▼
g0 調(diào)用 entersyscall()
 ?  ├── 保存 G1 上下文
 ?  ├── G1.status = _Gsyscall
 ?  ├── P 和 M 解綁(P 可以去找別的 M)
 ?  └── 釋放 P 的鎖
 ?  │
 ?  ▼
g0 執(zhí)行真正的 syscall(進(jìn)入內(nèi)核態(tài))
 ?  │
 ?  ▼
內(nèi)核完成 read,返回用戶態(tài)
 ?  │
 ?  ▼
g0 調(diào)用 exitsyscall()
 ?  ├── 嘗試重新綁定原來的 P
 ?  ├── 如果 P 被搶走了 → G1 進(jìn)全局隊(duì)列
 ?  └── 如果綁定成功 → 恢復(fù) G1 上下文
 ?  │
 ?  ▼
【切回 G1 用戶?!?
 ?  │
 ?  ▼
G1 繼續(xù)執(zhí)行

十一:GOMAXPROCS=4,100 萬個(gè) Goroutine,P 只有 4 個(gè),為什么不會(huì)成為瓶頸?

答案分三層:

① P 是"邏輯處理器",不是"執(zhí)行單元" 真正消耗 CPU 的是 M(OS 線程)。P 只是一個(gè)調(diào)度上下文,本身不消耗 CPU 時(shí)間。4 個(gè) P 意味著最多 4 個(gè) M 同時(shí)執(zhí)行用戶代碼,這與你的 4 核 CPU 完全匹配。

② 100 萬個(gè) G 大部分時(shí)間在睡覺 在真實(shí)的高并發(fā)程序中(如 Web 服務(wù)器),絕大多數(shù) G 處于:

  • 等待網(wǎng)絡(luò) I/O(netpoll 管理)
  • 等待 Channel
  • 等待鎖
  • 等待定時(shí)器

這些 G 不需要占用 P。只有就緒狀態(tài)(_Grunnable)的 G 才需要排隊(duì)等 P。通常就緒的 G 數(shù)量遠(yuǎn)小于 100 萬。

③ G 的本地隊(duì)列分散了壓力

  • 4 個(gè) P,每個(gè) P 的本地隊(duì)列可容納 256 個(gè) G。
  • 4 個(gè) P 合計(jì)能緩存 1024 個(gè)就緒 G,無需加鎖。
  • 全局隊(duì)列作為兜底,Work Stealing 保證負(fù)載均衡。

④ G 本身極輕量

  • 每個(gè) G 初始棧 2KB,100 萬個(gè) G 約 2GB 內(nèi)存。
  • 現(xiàn)代服務(wù)器輕松承受。且棧是按需增長的,很多 G 實(shí)際只占用幾 KB。

十二:純死循環(huán)for {},Go 1.14 前后分別發(fā)生什么?

Go 1.14 之前:

  • 協(xié)作式搶占,依賴函數(shù)調(diào)用序言檢查 preempt 標(biāo)志。
  • for {} 循環(huán)體里沒有任何函數(shù)調(diào)用,永遠(yuǎn)不會(huì)走到檢查點(diǎn)。
  • sysmon 雖然能檢測到 G 運(yùn)行超 10ms,也設(shè)置了 preempt 標(biāo)志,但 G 永遠(yuǎn)不去檢查。
  • 結(jié)果:這個(gè) G 永久霸占一個(gè) M 和 P,同 P 本地隊(duì)列里的其他 G 全部餓死。程序表現(xiàn)為"卡死"(部分邏輯不執(zhí)行)。

Go 1.14 之后:

  • sysmon 檢測到 G 運(yùn)行超 10ms,向 M 發(fā)送 SIGURG 信號。
  • M 的信號處理程序(gsignal)在循環(huán)的安全點(diǎn)(backedge)設(shè)置搶占標(biāo)記。
  • 即使 for {} 沒有函數(shù)調(diào)用,循環(huán)跳轉(zhuǎn)指令(JMP)本身被編譯器標(biāo)記為安全點(diǎn)。
  • 下一次循環(huán)迭代時(shí),檢查到搶占標(biāo)記,G 主動(dòng)讓出 CPU。
  • 結(jié)果:G 運(yùn)行約 10ms 后被強(qiáng)制切走,其他 G 得到執(zhí)行機(jī)會(huì)。

十三:棧切換時(shí)除了 SP/PC,還需要保存什么?為什么 BP 很重要?

gobuf 中保存的關(guān)鍵信息:

  • SP(棧指針):知道棧頂在哪,恢復(fù)后才能正確讀寫局部變量。
  • PC(程序計(jì)數(shù)器):知道下一條該執(zhí)行哪條指令,恢復(fù)后才能繼續(xù)執(zhí)行。
  • G(Goroutine 指針):知道當(dāng)前是哪個(gè) G,恢復(fù)后 M 的 curg 才能正確指向它。
  • BP(幀指針/基址指針):用于棧幀回溯。

為什么 BP 極其重要?

BP 指向當(dāng)前棧幀的底部。通過 BP 鏈表,可以回溯整個(gè)調(diào)用鏈:

當(dāng)前函數(shù)的 BP ──? 調(diào)用者的 BP ──? 調(diào)用者的調(diào)用者的 BP ──? ...

如果沒有 BP:

  • Panic 時(shí)無法打印堆棧runtime.Callersruntime.Stack 依賴 BP 鏈表回溯調(diào)用鏈。
  • GC 無法準(zhǔn)確掃描棧:GC 需要知道棧上哪些位置是指針。通過 BP 和編譯器生成的棧地圖(stack map),GC 才能正確識別存活對象。
  • Defer 和 Recover 無法定位defer 鏈表掛在棧幀上,沒有 BP 就找不到它們。

Go 在 AMD64 架構(gòu)上默認(rèn)啟用幀指針(-fno-omit-frame-pointer),就是為了保證調(diào)試和 GC 的正確性。

到此這篇關(guān)于Go語言中GMP調(diào)度模型詳解的文章就介紹到這了,更多相關(guān)go gmp調(diào)度模型內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • Golang使用Gin實(shí)現(xiàn)文件上傳的示例代碼

    Golang使用Gin實(shí)現(xiàn)文件上傳的示例代碼

    本文我們主要介紹了Golang如何使用Gin實(shí)現(xiàn)文件上傳,Go標(biāo)準(zhǔn)庫net/http對文件上傳已經(jīng)提供了非常完善的支持,而Gin框架在其基礎(chǔ)上進(jìn)一步封裝,因此使用Gin開發(fā)文件上傳功能時(shí),只需要簡單幾行代碼便可以實(shí)現(xiàn),需要的朋友可以參考下
    2024-02-02
  • 從基礎(chǔ)到高級詳解Go語言中錯(cuò)誤處理的實(shí)踐指南

    從基礎(chǔ)到高級詳解Go語言中錯(cuò)誤處理的實(shí)踐指南

    Go語言采用了一種獨(dú)特而明確的錯(cuò)誤處理哲學(xué),與其他主流編程語言形成鮮明對比,本文將為大家詳細(xì)介紹Go語言中錯(cuò)誤處理詳細(xì)方法,希望對大家有所幫助
    2025-09-09
  • Go語言利用泛型封裝常見的Map操作

    Go語言利用泛型封裝常見的Map操作

    Go?語言在?1.18?版本中引入了泛型,這是?Go?語言發(fā)展的一個(gè)重要里程碑,它極大地增強(qiáng)了語言的表達(dá)能力和靈活性,本文將通過泛型實(shí)現(xiàn)封裝常見的Map操作,感興趣的可以了解下
    2025-02-02
  • GoLang基于zap日志庫的封裝過程詳解

    GoLang基于zap日志庫的封裝過程詳解

    Zap是我個(gè)人比較喜歡的日志庫,是uber開源的,有較好的性能,在項(xiàng)目開發(fā)中,經(jīng)常需要把程序運(yùn)行過程中各種信息記錄下來,有了詳細(xì)的日志有助于問題排查和功能優(yōu)化,這篇文章主要介紹了GoLang基于zap日志庫的封裝過程,想要詳細(xì)了解可以參考下文
    2023-05-05
  • Golang巧用defer進(jìn)行錯(cuò)誤處理的方法

    Golang巧用defer進(jìn)行錯(cuò)誤處理的方法

    錯(cuò)誤處理是程序的重要組成部分,有效且優(yōu)雅的處理錯(cuò)誤是大多數(shù)程序員的追求,下面這篇文章主要給大家介紹了關(guān)于Golang中巧用defer進(jìn)行錯(cuò)誤處理的方法,文中通過示例介紹的非常詳細(xì),對大家具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來一起看看吧。
    2017-05-05
  • golang如何使用struct的tag屬性的詳細(xì)介紹

    golang如何使用struct的tag屬性的詳細(xì)介紹

    這篇文章主要介紹了golang如何使用struct的tag屬性的詳細(xì)介紹,從例子說起,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧
    2018-11-11
  • 5個(gè)可以在Golang中優(yōu)化代碼以提高性能的技巧分享

    5個(gè)可以在Golang中優(yōu)化代碼以提高性能的技巧分享

    作為一名軟件工程師,確保你的代碼高效且性能良好是非常重要的。本文主要和大家分享5個(gè)可以在Golang中優(yōu)化代碼以提高性能的技巧,希望對大家有所幫助
    2023-03-03
  • gin項(xiàng)目部署到服務(wù)器并后臺(tái)啟動(dòng)的步驟

    gin項(xiàng)目部署到服務(wù)器并后臺(tái)啟動(dòng)的步驟

    本文主要介紹了gin項(xiàng)目部署到服務(wù)器并后臺(tái)啟動(dòng)的步驟,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2023-02-02
  • Go語言for range(按照鍵值循環(huán))遍歷操作

    Go語言for range(按照鍵值循環(huán))遍歷操作

    這篇文章主要介紹了Go語言for range(按照鍵值循環(huán))遍歷操作,具有很好的參考價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧
    2020-12-12
  • golang內(nèi)存對齊詳解

    golang內(nèi)存對齊詳解

    在golang中,每一種數(shù)據(jù)類型都有其對應(yīng)的數(shù)據(jù)類型大小,也就是占用了多少內(nèi)存空間,我們可以通過unsafe.Sizeof函數(shù),來確定一個(gè)變量占用的內(nèi)存字節(jié)數(shù),本文將詳細(xì)給大家介紹golang內(nèi)存對齊,需要的朋友可以參考下
    2023-10-10

最新評論

凯里市| 洛阳市| 徐汇区| 集安市| 驻马店市| 信阳市| 郁南县| 镇康县| 古蔺县| 黎川县| 郁南县| 思南县| 阜南县| 仲巴县| 凌源市| 县级市| 宁波市| 界首市| 西乌珠穆沁旗| 苏尼特左旗| 托里县| 丹江口市| 古蔺县| 淄博市| 兴业县| 靖西县| 理塘县| 桑日县| 潼关县| 囊谦县| 菏泽市| 遂宁市| 贵州省| 巴南区| 黔东| 榆树市| 仪征市| 法库县| 闸北区| 黔江区| 高雄市|