雲栖号資訊:【 點選檢視更多行業資訊】
在這裡您可以找到不同行業的第一手的上雲資訊,還在等什麼,快來!
相信很多人都聽說過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語言中,Goroutine由一個名為runtime.go的結構體表示,該結構體非常複雜,有40多個成員變量,主要存儲執行棧、狀态、目前占用的線程、排程相關的資料。還有玩大家很想擷取的goroutine辨別,但是很抱歉,官方考慮到Go語言的發展,設定成私有了,不給你調用😏。
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,在協程切換、恢複上下文的時候用到。
sp uintptr
pc uintptr
g guintptr
ret sys.Uintreg
...
}
Machine
M就是對應作業系統的線程,最多會有GOMAXPROCS個活躍線程能夠正常運作,預設情況下GOMAXPROCS被設定為核心數,假如有四個核心,那麼預設就建立四個線程,每一個線程對應一個runtime.m結構體。線程數等于CPU個數的原因是,每個線程配置設定到一個CPU上就不至于出現線程的上下文切換,可以保證系統開銷降到最低。
M裡面存了兩個比較重要的東西,一個是g0,一個是curg。
g0:會深度參與運作時的排程過程,比如goroutine的建立、記憶體配置設定等
curg:代表目前正線上程上執行的goroutine。
剛才說P是負責M與G的關聯,是以M裡面還要存儲與P相關的資料。
- p:正在運作代碼的處理器
- nextp:暫存的處理器
- old:系統調用之前的線程的處理器
Processor
Proccessor負責Machine與Goroutine的連接配接,它能提供線程需要的上下文環境,也能配置設定G到它應該去的線程上執行,有了它,每個G都能得到合理的調用,每個線程都不再渾水摸魚,真是居家必備之良品。
同樣的,處理器的數量也是預設按照GOMAXPROCS來設定的,與線程的數量一一對應。
sp uintptr
pc uintptr
g guintptr
ret sys.Uintreg
...
}
結構體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年獲頒約翰·馮諾依曼獎。
【雲栖号線上課堂】每天都有産品技術專家分享!
課程位址:
https://yqh.aliyun.com/live立即加入社群,與專家面對面,及時了解課程最新動态!
【雲栖号線上課堂 社群】
https://c.tb.cn/F3.Z8gvnK
原文釋出時間:2020-04-17
本文作者:平也
本文來自:“
掘金”,了解相關資訊可以關注“掘金”