# Golang GMP模型 ###### tags: `Golang` ## 預備知識 ``` markmap # Buzzword ## 單/多核心 與 單/多工 ## kernel space 和 user space ## kernel mode 和 user mode ## process 和 thread ## kernel thread 和 user thread ## threading model ``` ### 單/多核心與單/多工 #### 單核心單工 Task A先執行,而Task B必須等待Task A執行完畢,Task C必須等待Task B執行完畢。 ``` mermaid gantt title Task A/B/C為順序執行 dateFormat hh:mm:ss.SSS axisFormat %M:%S section CPU Task A:t1, 12:00:00.000, 2s Task B:t2, after t1, 1s Task C:after t2, 3s ``` #### 單核心多工 Task A先執行一部分,之後換B,再來換C,就下來又換A執行剩下的部分。 ``` mermaid gantt title Task A/B/C為併發執行 dateFormat hh:mm:ss.SSS axisFormat %M:%S section CPU Task A-1:t1, 12:00:00.000, 1s Task B-1:t2, after t1, 1s Task C-1:t3, after t2, 1s Task A-2:t4, after t3, 1s Task B-2:t5, after t4, 1s Task C-2:after t5, 1s ``` 注意每個Task可以執行的時間(區塊)都是一個一個時間切面。 #### 多核心多工 ``` mermaid gantt title Task A/B/C/D為併發執行,任務分散到CPU 1/2上 dateFormat hh:mm:ss.SSS axisFormat %M:%S section CPU 1 Task A-1:t1, 12:00:00.000, 1s Task B-1:t2, after t1, 1s Task A-2:after t2, 1s section CPU 2 Task C-1:t3, 12:00:00.000, 1s Task D-1:t4, after t3, 1s ``` #### 任務切換 在上面多工的圖當中其實沒有畫出細節的部分,也就是併發執行當中,從一個task切換到另一個task的過程,也就是context switch。 ``` mermaid gantt title Context Switch dateFormat hh:mm:ss.SSS axisFormat %M:%S section CPU Task A-1:t1, 12:00:00.000, 1s context swtich: done, c1, 12:00:01.000, 12:00:01.200 Task B-1:after c1, 1s context swtich: done, c2, 12:00:02.200, 12:00:02.400 Task C-1:after c2, 1s context swtich: done, c3, 12:00:03.400, 12:00:03.600 ...: after c3, 1s ``` ### kernel mode 和 user mode * kernel mode:是最高權限的模式,可以存取硬碟,可以控制I/O,可以分配資源。 * user mode: 是最沒有權限的模式,只能做做基本的運算。process運行時,是藉由syscall來從user mode切換到kernel mode。 #### kernel的類型 * monolithic:大部分的系統功能(記憶體管理,排程管理,檔案系統,網路痛訊...等等)都是寫在kernel裡面,kernel較肥,因為大部分功能都在kernel裡,功能模組和功能模組間的呼叫只是func call而已,較有效率。 * 如Linux * microkernel:相對於monolithic,有些功能從kernel mode移出,成為了user mode的process,因此在呼叫對應功能時需要透過IPC交換訊息,IPC會帶來context switch和mode change,呼叫成本較高,但相對安全,因process掛掉不至於影響其他process的運作。 * 如L4 * 但不管kernel類型是什麼,都還是會區分成kernel mode或是user mode。 ### kernel space 和 user space * memory分成kernel space和user space * kernel space:kernel code儲存和kernel運作的。 * user space:一般常見user process運作的地方,而這個地方是由kernel來管理,避免user process亂存取到他人process。 **對memory存取的權限** |可以存取 | kernel space | user space | | -------- | -------- | -------- | | kernel mode | 可以 | 可以 | | user mode | 不行 | 可以 | ### process 和 thread #### Process * 為OS分配資源的最小單位 * 一個process至少有一個thread * 一個process可以有很多thread * process間的address space(記憶體中所能夠使用與控制的位址區段)是獨立的 * 持有自己的address space,包含code,data,stack和heap空間 * 持有自己的OS資源 #### Thread * 又名light weight process * 為OS排程的最小單位,無法獨立存在 * 同一個process中的thread,共享code,data區塊,但有自己的stack, register和PC * thread可以建立其他thread #### Task * 在Linux系統當中,process或是thread只有概念上差別,但具體表現process和thread都是同一個struct,叫做task_struct,所以可稱呼process和thread為task。 * task_struct裡面涵蓋描述process/thread的訊息,如pid, 狀態,記憶體配置, files,signal...等資訊 * 具體上區分process的方法是看task_struct裡面的指向mm_struct類型的指標,這個指標指到的記憶體位置是否是共用的。 > mm_struct 裡面就是存放著process的記憶體資訊,如code section, data section...的記憶體位置 ### kernel-level thread 和 user-level thread #### Kernel-level thread * 上述提到light weight process,就是kernel-level thread,會被scheduler來排程放置在CPU上運行的thread。也就是說,如果要建立一個kernel level thread,是需要呼叫system call並且由kernel來建立。 * 可被搶佔 #### User-level thread * 由語言自行實做,運行在user mode上的thread,依賴於kernel-level thread,OS無法感知這種thread,其排程也是由user mode上的排程器來進行管理,因此在做這層級的context-switch時,成本是比較小。 * 如pthread, java thread * 不可被搶佔 ### threading model 指的是在user-level thread如何和kernel-level thread去做對應。分為以下四種。 #### One-to-one 每建立一個user-level thread也會跟著生成kernel-level thread並且將之綁定。 ``` graphviz digraph one2one { subgraph cluster_level1{ label ="Kernel space"; kernel_thread1 [label="Thread1" shape=circle]; kernel_thread2 [label="Thread2" shape=circle]; kernel_thread3 [label="Thread3" shape=circle]; } subgraph cluster_level2{ label ="User space"; thread1 [label="Thread 1" shape=circle]; thread2 [label="Thread 2" shape=circle]; thread3 [label="Thread 3" shape=circle]; } thread1 -> kernel_thread1 thread2 -> kernel_thread2 thread3 -> kernel_thread3 } ``` #### Many-to-one 多對一的方式進行綁定。 ``` graphviz digraph many2one { subgraph cluster_level1{ label ="Kernel space"; kernel_thread [label="Thread" shape=circle]; } subgraph cluster_level2{ label ="User space"; thread1 [label="Thread 1" shape=circle]; thread2 [label="Thread 2" shape=circle]; thread3 [label="Thread 3" shape=circle]; } thread1 -> kernel_thread; thread2 -> kernel_thread; thread3 -> kernel_thread; } ``` #### Many-to-many user-level thread可以任意分配到kernel-level thread上執行。 ``` graphviz digraph many2many { subgraph cluster_level1{ label ="Kernel space"; kernel_thread1 [label="Thread 1" shape=circle]; kernel_thread2 [label="Thread 2" shape=circle]; } subgraph cluster_level2{ label ="User space"; thread1 [label="Thread 1" shape=circle]; thread2 [label="Thread 2" shape=circle]; thread3 [label="Thread 3" shape=circle]; } thread1 -> kernel_thread1; thread2 -> kernel_thread1; thread3 -> kernel_thread1; thread1 -> kernel_thread2; thread2 -> kernel_thread2; thread3 -> kernel_thread2; } ``` #### Two-level 與many-to-many相似,但可以允許指定做one-to-one ``` graphviz digraph many2many { subgraph cluster_level1{ label ="Kernel space"; kernel_thread1 [label="Thread 1" shape=circle]; kernel_thread2 [label="Thread 2" shape=circle]; kernel_thread3 [label="Thread 3" shape=circle]; } subgraph cluster_level2{ label ="User space"; thread1 [label="Thread 1" shape=circle]; thread2 [label="Thread 2" shape=circle]; thread3 [label="Thread 3" shape=circle]; thread4 [label="Thread 4" shape=circle]; } thread1 -> kernel_thread1; thread2 -> kernel_thread1; thread3 -> kernel_thread1; thread1 -> kernel_thread2; thread2 -> kernel_thread2; thread3 -> kernel_thread2; thread4 -> kernel_thread3; } ``` ## 正題 ### Goroutine Golang很好的封裝了user-level thread,並且提供了`go`關鍵字來建立一個thread,稱呼為Goroutine,同時在`runtime` package裡面定義了Go scheduler,用來排程和管理Goroutine們。 ### 什麼是GMP go scheduler所採用的調度模型稱呼為GMP(以前是GM),由G, M和P這三種元件構成。 #### G 就是Goroutine,可以視為user-level thread。如同 kernel-level thread是由OS做context switch在CPU core上運作,Goroutine是由Go scheduler做context switch在M上面運作。 Goroutine有三個狀態 * Waiting:代表Goroutine正在等待systemcall執行完畢,或是因為正在等待鎖。 * Runnable:代表Goroutine想要在M上執行指令。 * Executing:代表Goroutine正在M上執行指令當中。 #### M Machine的縮寫,代表的是kernel-level thread,由OS管理,是執行Goroutine的實體,預設最多可有10,000個M。 #### P Processor,是一個邏輯上意義的處理器,不代表真實的CPU core數量,這個數量在程序啟動時就被決定,這個也被代表著在process運行期間,最多同時只有#P個goroutine在運作,我們可以藉由P來限制process的併發程度。 #### LRQ Local Run Queue, 用來放置G,每個P都會擁有自己的LRQ。 #### GRQ Global Run Queue,也是用來放置G,當有些LRQ滿了之後,無法塞進更多G時,就會把G到GRQ裡面。 ``` graphviz digraph GMP { node[shape=record] rankdir=TB; GRQ [label="{ GRQ |{<Head>G|G|G|G|<Tail>}}"]; new [label="go\ func()" shape=none]; newG [label="G'" shape=circle]; new -> newG [label="new"]; newG -> P1:Tail [label="push"]; P1 [label="<P> P | { LRQ |{||||<Tail>}}"]; M1 [label="M" shape=triangle]; P1:P -> M1 [label="attach"]; RunG [label="G" shape=circle]; RunG -> M1 [label="run on"]; RunG -> new; GRQ ->P1:Tail [label="pull & push"]; P2 [label="<P>P | { LRQ |{<Head>G|G|G|G|G}}"]; M2 [label="M" shape=triangle]; P2:P -> M2; P2:Head->M2 [label="pull"]; Scheduler [label="OS Scheduler"]; M1 -> Scheduler; M2 -> Scheduler [label="managed by"]; Core1 [label="Core" shape=hexagon]; Core2 [label="Core" shape=hexagon]; Core3 [label="Core" shape=hexagon]; Core4 [label="Core" shape=hexagon]; Scheduler -> Core1; Scheduler -> Core2; Scheduler -> Core3; Scheduler -> Core4; } ``` ### 運作機制 * M必須先取得一個P後,才能從P的LRQ中取得G來執行 * 若是LRQ為空,則會從GRQ或是其他P的LRQ拿G來放到本地P的LRQ裡面 * M拿到G,執行G,執行到某個時間點後,會進行context switch,把G放回LRQ,並從P拿下一個G執行,一直重複上述步驟。 #### Handover 當G1正在M1上執行時,遇到了需要呼叫blocking system call時,為提高系統效能,Go scheduler會執行一種**Handover**的機制,如以下。 ``` graphviz digraph Handover1 { node[shape=record] rankdir=TB; P [label="<P> P | { LRQ |{G2|G3|G4|G5}}"]; M1 [label="M1" shape=triangle]; P:P -> M1 ; G1 [label="G1" shape=circle]; G1 -> M1; } ``` 這時G1想要呼叫blocking system call,這時會將M1和P解除關聯,並取得一個新的M(M2)來和原P進行關聯,並繼續執行LRQ內的G2。而M1則是持續負責G1的執行。 ``` graphviz digraph Handover2 { node[shape=record] rankdir=TB; P [label="<P> P | { LRQ |{G3|G4|G5|}}"]; M1 [label="M1" shape=triangle]; M2 [label="M2" shape=triangle]; P:P -> M2 ; G2 -> M2; G1 [label="G1" shape=circle]; G1 -> M1; } ``` 當G1執行system call完畢後,會被塞回LRQ,而M1則變成閒置狀態,並等待之後使用。 ``` graphviz digraph Handover2 { node[shape=record] rankdir=TB; P [label="<P> P | { LRQ |{G3|G4|G5|G1}}"]; M1 [label="M1" shape=triangle]; M2 [label="M2" shape=triangle]; P:P -> M2 ; G2 -> M2; } ``` #### Work stealing 如果有個P的LRQ已經空了,且M也沒有正在執行的G,那這個M因為進入到閒置狀態很有可能被OS scheduler context switch掉,即使這個process中還有其他待執行的G。為了避免上述的情況發生,這時Go scheduler會執行work stealing,從其他的P LRQ中或是GRQ中偷G。 一開始執行狀態如下圖。 ``` graphviz digraph workstealing1 { node[shape=record] rankdir=TB; P1 [label="<P> P1 | { LRQ |{G3|G5|G7}}"]; M1 [label="M1" shape=triangle]; P1:P -> M1 ; G1 [label="G1" shape=circle]; G1 -> M1; P2 [label="<P> P2 | { LRQ |{G4|G6|G8}}"]; M2 [label="M2" shape=triangle]; P2:P -> M2 ; G2 [label="G2" shape=circle]; G2 -> M2; GRQ [label="{ GRQ |{G9||}}"]; } ``` 而當M1及其對應的P1 LRQ為空時。 ``` graphviz digraph workstealing1 { node[shape=record] rankdir=TB; P1 [label="<P> P1 | { LRQ |{||}}"]; M1 [label="M1" shape=triangle]; P1:P -> M1 ; P2 [label="<P> P2 | { LRQ |{G4|G6|G8}}"]; M2 [label="M2" shape=triangle]; P2:P -> M2 ; G2 [label="G2" shape=circle]; G2 -> M2; GRQ [label="{ GRQ |{G9||}}"]; } ``` P1嘗試從P2的LRQ偷一半的G,結果如下。 ``` graphviz digraph workstealing1 { node[shape=record] rankdir=TB; P1 [label="<P> P1 | { LRQ |{G6||}}"]; M1 [label="M1" shape=triangle]; P1:P -> M1 ; G4 [label="G4" shape=circle]; G4 -> M1; P2 [label="<P> P2 | { LRQ |{G8||}}"]; M2 [label="M2" shape=triangle]; P2:P -> M2 ; G2 [label="G2" shape=circle]; G2 -> M2; GRQ [label="{ GRQ |{G9||}}"]; } ``` 而當M2把P2的G都執行完畢時,且這時候P1的LRQ也空了。 ``` graphviz digraph workstealing1 { node[shape=record] rankdir=TB; P1 [label="<P> P1 | { LRQ |{||}}"]; M1 [label="M1" shape=triangle]; P1:P -> M1 ; G6 [label="G6" shape=circle]; G6 -> M1; P2 [label="<P> P2 | { LRQ |{||}}"]; M2 [label="M2" shape=triangle]; P2:P -> M2 ; GRQ [label="{ GRQ |{G9||}}"]; } ``` P2這時候會嘗試從GRQ偷,如下。 ``` graphviz digraph workstealing1 { node[shape=record] rankdir=TB; P1 [label="<P> P1 | { LRQ |{||}}"]; M1 [label="M1" shape=triangle]; P1:P -> M1 ; G6 [label="G6" shape=circle]; G6 -> M1; P2 [label="<P> P2 | { LRQ |{||}}"]; M2 [label="M2" shape=triangle]; P2:P -> M2 ; G9 [label="G9" shape=circle]; G9 -> M1; GRQ [label="{ GRQ |{||}}"]; } ``` ## Reference 1. http://www.it.uu.se/education/course/homepage/os/vt18/module-4/implementing-threads/ 2. https://www.ardanlabs.com/blog/2018/08/scheduling-in-go-part1.html