??我是平也,這有一個專注Gopher技術成長的開源專案「go home」
導讀
相信很多人都聽說過Go語言天然支持高并發,原因是內部有協程(goroutine)加持,可以在一個行程中啟動成千上萬個協程,那么,它憑什么做到如此高的并發呢?那就需要先了解什么是并發模型,

并發模型
著名的C++專家Herb Sutter曾經說過“免費的午餐已經終結”,為了讓代碼運行的更快,單純依靠更快的硬體已經無法得到滿足,我們需要利用多核來挖掘并行的價值,而并發模型的目的就是來告訴你不同執行物體之間是如何協作的,

當然,不同的并發模型的協作方式也不盡相同,常見的并發模型有七種:
- 執行緒與鎖
- 函式式編程
- Clojure之道
- actor
- 通訊順序行程(CSP)
- 資料級并行
- Lambda架構
而今天,我們只講與Go語言相關的并發模型CSP,感興趣的同學可以自行查閱書籍《七周七并發模型》,

CSP篇
CSP,全稱Communicating Sequential Processes,意為通訊順序行程,它是七大并發模型中的一種,它的核心觀念是將兩個并發執行的物體通過通道channel連接起來,所有的訊息都通過channel傳輸,其實CSP概念早在1978年就被東尼·霍爾提出,由于近來Go語言的興起,CSP又火了起來,
那么CSP與Go語言有什么關系呢?接下來我們來看Go語言對CSP并發模型的實作——GPM調度模型,

GPM調度模型
GPM代表了三個角色,分別是Goroutine、Processor、Machine,

- Goroutine:就是咱們常用的用go關鍵字創建的執行體,它對應一個結構體g,結構體里保存了goroutine的堆疊資訊
- Machine:表示作業系統的執行緒
- Processor:表示處理器,有了它才能建立G、M的聯系
Goroutine
Goroutine就是代碼中使用go關鍵詞創建的執行單元,也是大家熟知的有“輕量級執行緒”之稱的協程,協程是不為作業系統所知的,它由編程語言層面實作,背景關系切換不需要經過內核態,再加上協程占用的記憶體空間極小,所以有著非常大的發展潛力,
go func() {}()
在Go語言中,Goroutine由一個名為runtime.go的結構體表示,該結構體非常復雜,有40多個成員變數,主要存盤執行堆疊、狀態、當前占用的執行緒、調度相關的資料,還有玩大家很想獲取的goroutine標識,但是很抱歉,官方考慮到Go語言的發展,設定成私有了,不給你呼叫??,
type g struct {
stack struct {
lo uintptr
hi uintptr
} // 堆疊記憶體:[stack.lo, stack.hi)
stackguard0 uintptr
stackguard1 uintptr
_panic *_panic
_defer *_defer
m *m // 當前的 m
sched gobuf
stktopsp uintptr // 期望 sp 位于堆疊頂,用于回溯檢查
param unsafe.Pointer // wakeup 喚醒時候傳遞的引數
atomicstatus uint32
goid int64
preempt bool // 搶占信號,stackguard0 = stackpreempt 的副本
timer *timer // 為 time.Sleep 快取的計時器
...
}
Goroutine調度相關的資料存盤在sched,在協程切換、恢復背景關系的時候用到,
type gobuf struct {
sp uintptr
pc uintptr
g guintptr
ret sys.Uintreg
...
}
Machine
M就是對應作業系統的執行緒,最多會有GOMAXPROCS個活躍執行緒能夠正常運行,默認情況下GOMAXPROCS被設定為內核數,假如有四個內核,那么默認就創建四個執行緒,每一個執行緒對應一個runtime.m結構體,執行緒數等于CPU個數的原因是,每個執行緒分配到一個CPU上就不至于出現執行緒的背景關系切換,可以保證系統開銷降到最低,
type m struct {
g0 *g
curg *g
...
}
M里面存了兩個比較重要的東西,一個是g0,一個是curg,
- g0:會深度參與運行時的調度程序,比如goroutine的創建、記憶體分配等
- curg:代表當前正在執行緒上執行的goroutine,
剛才說P是負責M與G的關聯,所以M里面還要存盤與P相關的資料,
type m struct {
...
p puintptr
nextp puintptr
oldp puintptr
}
- p:正在運行代碼的處理器
- nextp:暫存的處理器
- old:系統呼叫之前的執行緒的處理器
Processor
Proccessor負責Machine與Goroutine的連接,它能提供執行緒需要的背景關系環境,也能分配G到它應該去的執行緒上執行,有了它,每個G都能得到合理的呼叫,每個執行緒都不再渾水摸魚,真是居家必備之良品,

同樣的,處理器的數量也是默認按照GOMAXPROCS來設定的,與執行緒的數量一一對應,
type p struct {
m muintptr
runqhead uint32
runqtail uint32
runq [256]guintptr
runnext guintptr
...
}
結構體P中存盤了性能追蹤、垃圾回收、計時器等相關的欄位外,還存盤了處理器的待運行佇列,佇列中存盤的是待執行的Goroutine串列,
三者的關系
首先,默認啟動四個執行緒四個處理器,然后互相系結,

這個時候,一個Goroutine結構體被創建,在進行函式體地址、引數起始地址、引數長度等資訊以及調度相關屬性更新之后,它就要進到一個處理器的佇列等待發車,

啥,又創建了一個G?那就輪流往其他P里面放唄,相信你排隊取號的時候看到其他視窗沒人排隊也會過去的,

假如有很多G,都塞滿了怎么辦呢?那就不把G塞到處理器的私有佇列里了,而是把它塞到全域佇列里(候車大廳),

除了往里塞之外,M這邊還要瘋狂往外取,首先去處理器的私有佇列里取G執行,如果取完的話就去全域佇列取,如果全域佇列里也沒有的話,就去其他處理器佇列里偷,哇,這么饑渴,簡直是惡魔啊!

如果哪里都沒找到要執行的G呢?那M就會因為太失望和P斷開關系,然后去睡覺(idle)了,

那如果兩個Goroutine正在通過channel做一些恩恩愛愛的事阻塞住了怎么辦,難道M要等他們完事了再繼續執行?顯然不會,M并不稀罕這對Go男女,而會轉身去找別的G執行,

系統呼叫
如果G進行了系統呼叫syscall,M也會跟著進入系統呼叫狀態,那么這個P留在這里就浪費了,怎么辦呢?這點精妙之處在于,P不會傻傻的等待G和M系統呼叫完成,而會去找其他比較閑的M執行其他的G,

當G完成了系統呼叫,因為要繼續往下執行,所以必須要再找一個空閑的處理器發車,

如果沒有空閑的處理器了,那就只能把G放回全域佇列當中等待分配,

sysmon
sysmon是我們的保潔阿姨,它是一個M,又叫監控執行緒,不需要P就可以獨立運行,每20us~10ms會被喚醒一次出來打掃衛生,主要作業就是回收垃圾、回收長時間系統調度阻塞的P、向長時間運行的G發出搶占調度等等,
詞條解釋
東尼·霍爾
東尼·霍爾,英國計算機科學家,圖靈獎得主,他設計了牛氣沖天的快速排序演算法、霍爾邏輯以及CSP模型,2011年獲頒約翰·馮諾依曼獎,

感謝大家的觀看,如果覺得文章對你有所幫助,歡迎關注公眾號「平也」,聚焦Go語言與技術原理,

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/24403.html
標籤:Go
