go中的關鍵字-go(上)
1. goroutine的使用
在Go語言中,表示式go f(x, y, z)會啟動一個新的goroutine執行函式f(x, y, z),建立一個併發任務單元。即go關鍵字可以用來開啟一個goroutine(協程))進行任務處理。
建立單個goroutine
1 package main 2 3 import ( 4 "fmt" 5 ) 6 7 func HelloWorld() { 8 fmt.Println("Hello goroutine") 9 } 10 11 func main() { 12 go HelloWorld() // 開啟一個新的併發執行
time.Sleep(1*time.Second) 13 fmt.Println("後輸出訊息!") 14 }
輸出
1 Hello goroutine 2 後輸出訊息!
這裡的sleep是必須的,否則你可能看不到goroutine裡頭的輸出,或者裡面的訊息後輸出。因為當main函式返回時,所有的gourutine都是暴力終結的,然後程式退出。
建立多個goroutine時
1 package main 2 3 import ( 4 "fmt" 5 "time" 6 ) 7 8 func DelayPrint() { 9 for i := 1; i <= 3; i++ { 10 time.Sleep(500 * time.Millisecond) 11 fmt.Println(i) 12 } 13 } 14 15 func HelloWorld() { 16 fmt.Println("Hello goroutine") 17 } 18 19 func main() { 20 go DelayPrint() // 第一個goroutine 21 go HelloWorld() // 第二個goroutine 22 time.Sleep(10*time.Second) 23 fmt.Println("main func") 24 }
輸出
1 Hello goroutine 2 1 3 2 4 3 5 4 6 7 main func
當去掉 DelayPrint() 函式裡的sleep之後,輸出為:
1 1 2 2 3 3 4 4 5 Hello goroutine 6 main function
說明第二個goroutine不會因為第一個而堵塞或者等待。事實是當程式執行go FUNC()的時候,只是簡單的呼叫然後就立即返回了,並不關心函式裡頭髮生的故事情節,所以不同的goroutine直接不影響,main會繼續按順序執行語句。
goroutine阻塞
場景一:
1 package main 2 3 func main() { 4 ch := make(chan int) 5 <- ch // 阻塞main goroutine, 通道被鎖 6 }
執行程式會報錯:
1 fatal error: all goroutines are asleep - deadlock! 2 3 goroutine 1 [chan receive]: 4 main.main()
場景二
1 package main 2 3 func main() { 4 ch1, ch2 := make(chan int), make(chan int) 5 6 go func() { 7 ch1 <- 1 // ch1通道的資料沒有被其他goroutine讀取走,堵塞當前goroutine 8 ch2 <- 0 9 }() 10 11 <- ch2 // ch2 等待資料的寫 12 }
非緩衝通道上如果只有資料流入,而沒有流出,或者只流出無流入,都會引起阻塞。 goroutine的非緩衝通道里頭一定要一進一出,成對出現。 上面例子,一:流出無流入;二:流入無流出。
處理方式:
1. 讀取通道資料
1 package main 2 3 func main() { 4 ch1, ch2 := make(chan int), make(chan int) 5 6 go func() { 7 ch1 <- 1 // ch1通道的資料沒有被其他goroutine讀取走,堵塞當前goroutine 8 ch2 <- 0 9 }() 10 11 <- ch1 // 取走便是 12 <- ch2 // chb 等待資料的寫 13 }
2. 建立緩衝通道
1 package main 2 3 func main() { 4 ch1, ch2 := make(chan int, 3), make(chan int) 5 6 go func() { 7 ch1 <- 1 // cha通道的資料沒有被其他goroutine讀取走,堵塞當前goroutine 8 ch2 <- 0 9 }() 10 11 <- ch2 // ch2 等待資料的寫 12 }
2. goroutine排程器相關結構
goroutine的排程涉及到幾個重要的資料結構,我們先逐一介紹和分析這幾個資料結構。這些資料結構分別是結構體G,結構體M,結構體P,以及Sched結構體。前三個的定義在檔案runtime/runtime.h中,而Sched的定義在runtime/proc.c中。Go語言的排程相關實現也是在檔案proc.c中。
2.1 結構體G
g是goroutine的縮寫,是goroutine的控制結構,是對goroutine的抽象。看下它內部主要的一些結構:
1 type g struct { 2 //堆疊引數。 3 //堆疊描述了實際的堆疊記憶體:[stack.lo,stack.hi)。 4 // stackguard0是在Go堆疊增長序言中比較的堆疊指標。 5 //通常是stack.lo + StackGuard,但是可以通過StackPreempt觸發搶佔。 6 // stackguard1是在C堆疊增長序言中比較的堆疊指標。 7 //它是g0和gsignal堆疊上的stack.lo + StackGuard。 8 //在其他goroutine堆疊上為〜0,以觸發對morestackc的呼叫(並崩潰)。
9 //當前g使用的棧空間,stack結構包括 [lo, hi]兩個成員 10 stack stack // offset known to runtime/cgo
11 // 用於檢測是否需要進行棧擴張,go程式碼使用 12 stackguard0 uintptr // offset known to liblink
13 // 用於檢測是否需要進行棧擴充套件,原生程式碼使用的 14 stackguard1 uintptr // offset known to liblink
15 // 當前g所繫結的m 16 m *m // current m; offset known to arm liblink
17 // 當前g的排程資料,當goroutine切換時,儲存當前g的上下文,用於恢復 18 sched gobuf
19 // goroutine執行的函式 20 fnstart *FuncVal 21 // g當前的狀態 22 atomicstatus uint32 23 // 當前g的id 24 goid int64
25 // 狀態Gidle,Grunnable,Grunning,Gsyscall,Gwaiting,Gdead 26 status int16
27 // 下一個g的地址,通過guintptr結構體的ptr set函式可以設定和獲取下一個g,通過這個欄位和sched.gfreeStack sched.gfreeNoStack 可以把 free g串成一個連結串列 28 schedlink guintptr
29 // 判斷g是否允許被搶佔 30 preempt bool // preemption signal, duplicates stackguard0 = stackpreempt
31 // g是否要求要回到這個M執行, 有的時候g中斷了恢復會要求使用原來的M執行 32 lockedm muintptr
33 // 用於傳遞引數,睡眠時其它goroutine設定param,喚醒時此goroutine可以獲取
param *void
34 // 建立這個goroutine的go表示式的pc 35 uintptr gopc 36 }
其中包含了棧資訊stackbase和stackguard,有執行的函式資訊fnstart。這些就足夠成為一個可執行的單元了,只要得到CPU就可以執行。goroutine切換時,上下文資訊儲存在結構體的sched域中。goroutine切換時,上下文資訊儲存在結構體的sched域中。goroutine是輕量級的執行緒
或者稱為協程
,切換時並不必陷入到作業系統核心中,很輕量級。
結構體G中的Gobuf,其實只儲存了當前棧指標,程式計數器,以及goroutine自身。
1 struct Gobuf 2 { 3 //這些欄位的偏移是libmach已知的(硬編碼的)。 4 sp uintper; 5 pc *byte; 6 g *G; 7 ... 8 };
記錄g是為了恢復當前goroutine的結構體G指標,執行時庫中使用了一個常駐的暫存器extern register G* g
,這是當前goroutine的結構體G的指標。這種結構是為了快速地訪問goroutine中的資訊,比如,Go的棧的實現並沒有使用%ebp暫存器,不過這可以通過g->stackbase快速得到。"extern register"是由6c,8c等實現的一個特殊的儲存,在ARM上它是實際的暫存器。在linux系統中,對g和m使用的分別是0(GS)和4(GS)。連結器還會根據特定作業系統改變編譯器的輸出,每個連結到Go程式的C檔案都必須包含runtime.h標頭檔案,這樣C編譯器知道避免使用專用的暫存器。
2.2 結構體P
P是Processor的縮寫。結構體P的加入是為了提高Go程式的併發度,實現更好的排程。M代表OS執行緒。P代表Go程式碼執行時需要的資源。
1 type p struct { 2 lock mutex 3 4 id int32 5 // p的狀態,稍後介紹 6 status uint32 // one of pidle/prunning/... 7 8 // 下一個p的地址,可參考 g.schedlink 9 link puintptr 10 // p所關聯的m 11 m muintptr // back-link to associated m (nil if idle) 12 13 // 記憶體分配的時候用的,p所屬的m的mcache用的也是這個 14 mcache *mcache 15 16 // Cache of goroutine ids, amortizes accesses to runtime·sched.goidgen. 17 // 從sched中獲取並快取的id,避免每次分配goid都從sched分配 18 goidcache uint64 19 goidcacheend uint64 20 21 // Queue of runnable goroutines. Accessed without lock. 22 // p 本地的runnbale的goroutine形成的佇列 23 runqhead uint32 24 runqtail uint32 25 runq [256]guintptr 26 27 // runnext,如果不是nil,則是已準備好執行的G 28 //當前的G,並且應該在下一個而不是其中執行 29 // runq,如果執行G的時間還剩時間 30 //切片。它將繼承當前時間剩餘的時間 31 //切片。如果一組goroutine鎖定在 32 //交流等待模式,該計劃將其設定為 33 //單位並消除(可能很大)排程 34 //否則會由於新增就緒商品而引起的延遲 35 // goroutines到執行佇列的末尾。 36 37 // 下一個執行的g,如果是nil,則從佇列中獲取下一個執行的g 38 runnext guintptr 39 40 // Available G's (status == Gdead) 41 // 狀態為 Gdead的g的列表,可以進行復用 42 gfree *g 43 gfreecnt int32 44 }
跟G不同的是,P不存在waiting
狀態。MCache被移到了P中,但是在結構體M中也還保留著。在P中有一個Grunnable的goroutine佇列,這是一個P的區域性佇列。當P執行Go程式碼時,它會優先從自己的這個區域性佇列中取,這時可以不用加鎖,提高了併發度。如果發現這個佇列空了,則去其它P的佇列中拿一半過來,這樣實現工作流竊取的排程。這種情況下是需要給呼叫器加鎖的。
2.3 結構體M
M是machine的縮寫,是對機器的抽象,每個m都是對應到一條作業系統的物理執行緒。
1 type m struct { 2 // g0是用於排程和執行系統呼叫的特殊g 3 g0 *g // goroutine with scheduling stack 4 // m當前執行的g 5 curg *g // current running goroutine 6 // 當前擁有的p 7 p puintptr // attached p for executing go code (nil if not executing go code)
8 // 執行緒的 local storage 9 tls [6]uintptr // thread-local storage 10 // 喚醒m時,m會擁有這個p 11 nextp puintptr 12 id int64 13 // 如果 !="", 繼續執行curg 14 preemptoff string // if != "", keep curg running on this m
15 // 自旋狀態,用於判斷m是否工作已結束,並尋找g進行工作 16 spinning bool // m is out of work and is actively looking for work
17 // 用於判斷m是否進行休眠狀態 18 blocked bool // m is blocked on a note 19 // m休眠和喚醒通過這個,note裡面有一個成員key,對這個key所指向的地址進行值的修改,進而達到喚醒和休眠的目的 20 park note
21 // 所有m組成的一個連結串列 22 alllink *m // on allm 23 // 下一個m,通過這個欄位和sched.midle 可以串成一個m的空閒連結串列 24 schedlink muintptr 25 // mcache,m擁有p的時候,會把自己的mcache給p 26 mcache *mcache 27 // lockedm的對應值 28 lockedg guintptr 29 // 待釋放的m的list,通過sched.freem 串成一個連結串列 30 freelink *m // on sched.freem 31 }
和G類似,M中也有alllink域將所有的M放在allm連結串列中。lockedg是某些情況下,G鎖定在這個M中執行而不會切換到其它M中去。M中還有一個MCache,是當前M的記憶體的快取。M也和G一樣有一個常駐暫存器變數,代表當前的M。同時存在多個M,表示同時存在多個物理執行緒。
2.4 Sched結構體
Sched是排程實現中使用的資料結構,該結構體的定義在檔案proc.c中。
1 type schedt struct { 2 // 全域性的go id分配 3 goidgen uint64 4 // 記錄的最後一次從i/o中查詢g的時間 5 lastpoll uint64 6 7 lock mutex 8 9 //當增加nmidle,nmidlelocked,nmsys或nmfreed時,應 10 //確保呼叫checkdead()。 11 12 // m的空閒連結串列,結合m.schedlink 就可以組成一個空閒連結串列了 13 midle muintptr // idle m's waiting for work 14 nmidle int32 // number of idle m's waiting for work 15 nmidlelocked int32 // number of locked m's waiting for work 16 // 下一個m的id,也用來記錄建立的m數量 17 mnext int64 // number of m's that have been created and next M ID 18 // 最多允許的m的數量 19 maxmcount int32 // maximum number of m's allowed (or die) 20 nmsys int32 // number of system m's not counted for deadlock 21 // free掉的m的數量,exit的m的數量 22 nmfreed int64 // cumulative number of freed m's 23 24 ngsys uint32 // 系統goroutine的數量;原子更新 25 26 pidle puintptr // 閒置的 27 npidle uint32 28 nmspinning uint32 // See "Worker thread parking/unparking" comment in proc.go. 29 30 // Global runnable queue. 31 // 這個就是全域性的g的隊列了,如果p的本地佇列沒有g或者太多,會跟全域性佇列進行平衡 32 // 根據runqhead可以獲取佇列頭的g,然後根據g.schedlink 獲取下一個,從而形成了一個連結串列 33 runqhead guintptr 34 runqtail guintptr 35 runqsize int32 36 37 // freem是m等待被釋放時的列表 38 //設定了m.exited。通過m.freelink連結。 39 40 // 等待釋放的m的列表 41 freem *m 42 }
大多數需要的資訊都已放在了結構體M、G和P中,Sched結構體只是一個殼。可以看到,其中有M的idle佇列,P的idle佇列,以及一個全域性的就緒的G佇列。Sched結構體中的Lock是非常必須的,如果M或P等做一些非區域性的操作,它們一般需要先鎖住排程器。
3. G、P、M相關狀態
g.status
- _Gidle: goroutine剛剛建立還沒有初始化
- _Grunnable: goroutine處於執行佇列中,但是還沒有執行,沒有自己的棧
- _Grunning: 這個狀態的g可能處於執行使用者程式碼的過程中,擁有自己的m和p
- _Gsyscall: 執行systemcall中
- _Gwaiting: 這個狀態的goroutine正在阻塞中,類似於等待channel
- _Gdead: 這個狀態的g沒有被使用,有可能是剛剛退出,也有可能是正在初始化中
- _Gcopystack: 表示g當前的棧正在被移除,新棧分配中
p.status
- _Pidle: 空閒狀態,此時p不繫結m
- _Prunning: m獲取到p的時候,p的狀態就是這個狀態了,然後m可以使用這個p的資源執行g
- _Psyscall: 當go呼叫原生程式碼,原生程式碼又反過來呼叫go的時候,使用的p就會變成此態
- _Pdead: 當執行中,需要減少p的數量時,被減掉的p的狀態就是這個了
m.status
m的status沒有p、g的那麼明確,但是在執行流程的分析中,主要有以下幾個狀態
- 執行中: 拿到p,執行g的過程中
- 執行原生程式碼: 正在執行原聲程式碼或者阻塞的syscall
- 休眠中: m發現無待執行的g時,進入休眠,並加入到空閒列表中
- 自旋中(spining): 當前工作結束,正在尋找下一個待執行的g
4. G、P、M的排程關係
一個G就是一個gorountine,儲存了協程的棧、程式計數器以及它所在M的資訊。P全稱是Processor,處理器,它的主要用途就是用來執行goroutine的。M代表核心級執行緒,一個M就是一個執行緒,goroutine就是跑在M之上的。程式啟動時,會建立一個主G,而每使用一次go關鍵字也建立一個G。go func()建立一個新的G後,放到P的本地佇列裡,或者平衡到全域性佇列,然後檢查是否有可用的M,然後喚醒或新建一個M,M獲取待執行的G和空閒的P,將呼叫引數儲存到g的棧,將sp,pc等上下文環境儲存在g的sched域,這樣整個goroutine就準備好了,只要等分配到CPU,它就可以繼續執行,之後再清理現場,重新進入排程迴圈。
4.1 排程實現
圖中有兩個物理執行緒,M0、M1每一個M都擁有一個處理器P,每一個P都有一個正在執行的G。P的數量可以通過GOMAXPROCS()來設定,它其實也代表了真正的併發度,即有多少個goroutine可以同時執行。圖中灰色goroutine都是處於ready的就緒態,正在等待被排程。由P維護這個就緒佇列(runqueue),go function每啟動一個goroutine,runqueue佇列就在其末尾加入一個goroutine,在下一個排程點,就從runqueue中取出一個goroutine執行。
當一個OS執行緒M0陷入阻塞時,P轉而在M1上執行G,圖中的M1可能是正被建立,或者從執行緒快取中取出。當MO返回時,它嘗試取得一個P來執行goroutine,一般情況下,它會從其他的OS執行緒那裡拿一個P過來執行,像M1獲取P一樣;如果沒有拿到的話,它就把goroutine放在一個global runqueue(全域性執行佇列)裡,然後自己睡眠(放入執行緒快取裡)。所有的P會週期性的檢查全域性佇列並執行其中的goroutine,否則其上的goroutine永遠無法執行。
另一種情況是P上的任務G很快就執行完了(分配不均),這個處理器P很忙,但是其他的P還有任務,此時如果global runqueue也沒有G了,那麼P就會從其他的P裡拿一些G來執行。一般來說,如果一般就拿run queue的一半,這就確保了每個OS執行緒都能充分的使用。
golang採用了m:n執行緒模型,即m個gorountine(簡稱為G)對映到n個使用者態程序(簡稱為P)上,多個G對應一個P,一個P對應一個核心執行緒(簡稱為M)。 P的數量:由啟動時環境變數$GOMAXPROCS或者是由runtime的方法GOMAXPROCS()決定(預設是1)。這意味著在程式執行的任意時刻都只有$GOMAXPROCS個goroutine在同時執行。在確定了P的最大數量n後,執行時系統會根據這個數量建立n個P。M的數量:go語言本身的限制:go程式啟動時,會設定M的最大數量,預設10000.但是核心很難支援這麼多的執行緒數,所以這個限制可以忽略。runtime/debug中的SetMaxThreads函式,設定M的最大數量。一個M阻塞了,會建立新的M。
M與P的數量沒有絕對關係,一個M阻塞,P就會去建立或者切換另一個M,所以,即使P的預設數量是1,也有可能會建立很多個M出來。
P上G的排程:如果一個G不主動讓出cpu或被動block,所屬P中的其他G會一直等待順序執行。
一個G執行IO時可能會進入waiting狀態,主動讓出CPU,此時會被移到所屬P中的其他G後面,等待下一次輪到執行。 一個G呼叫了runtime.Gosched()會進入runnable狀態,主動讓出CPU,並被放到全域性等待佇列中。 一個G呼叫了runtime.Goexit(),該G將會被立即終止,然後把已載入的defer(有點類似析構)依次執行完。 一個G呼叫了允許block的syscall,此時G及其對應的P、其他G和M都會被block起來,監控執行緒M會定時掃描所有P,一旦發現某個P處於block syscall狀態,則通知排程器讓另一個M來帶走P(這裡的另一個M可能是新建立的,因此隨著G被不斷block,M數量會不斷增加,最終M數量可能會超過P數量),這樣P及其餘下的G就不會被block了,等被block的M返回時發現自己的P沒有了,也就不能再處理G了,於是將G放入全域性等待佇列等待空閒P接管,然後M自己sleep。 通過實驗,當一個G運行了很久(比如進入死迴圈),會被自動切到其他CPU核,可能是因為超過時間片後G被移到全域性等待佇列中,後面被其他CPU核上的M處理。M上P和G的排程:每當一個G要開始執行時,排程器判斷當前M的數量是否可以很好處理完G:如果M少G多且有空閒P,則新建M或喚醒一個sleep M,並指定使用某個空閒P;如果M應付得來,G被負載均衡放入一個現有P+M中。
當M處理完其身上的所有G後,會再去全域性等待佇列中找G,如果沒有就從其他P中分一半的G(以便保證各個M處理G的負載大致相等),如果還沒有,M就去sleep了,對應的P變為空閒P。 在M進入sleep期間,排程器可能會給其P不斷放入G,等M醒後(比如超時):如果G數量不多,則M直接處理這些G;如果M覺得G太多且有空閒P,會先主動喚醒其他sleep的M來分擔G,如果沒有其他sleep的M,排程器建立新M來分擔。協程特點
協程擁有自己的暫存器上下文和棧。協程排程切換時,將暫存器上下文和棧儲存到其他地方,在切回來的時候,恢復先前儲存的暫存器上下文和棧。因此,協程能保留上一次呼叫時的狀態(即所有區域性狀態的一個特定組合),每次過程重入時,就相當於進入上一次呼叫的狀態,換種說法:進入上一次離開時所處邏輯流的位置。執行緒和程序的操作是由程式觸發系統介面,最後的執行者是系統;協程的操作執行者則是使用者自身程式,goroutine也是協程。
&n