學(xué)習(xí)golang滔驶,必然繞不開并行與并發(fā)這樣的概念遇革,作為golang的一大賣點(diǎn),輕量級的協(xié)程應(yīng)該是我們要理解的揭糕,包括他的調(diào)度萝快。
而且,雖然golang的goroutine輕量級著角,方便大量創(chuàng)建揪漩,但是在高度并發(fā)的情況下,也會出現(xiàn)一些問題吏口。
Goroutine & Scheduler
首先奄容,通常我們會把golang的goroutine簡單的理解為coroutine(協(xié)程),但是實(shí)際上兩者是有差別的产徊,現(xiàn)在主流的線程模型分為3種:內(nèi)核級線程模型昂勒,用戶級線程模型,和兩級線程模型(也稱為混合線程模型)舟铜。
傳統(tǒng)的協(xié)程屬于用戶級線程模型戈盈,而goroutine和他的go scheduler在底層實(shí)現(xiàn)上實(shí)際上屬于兩級線程模型。
所以兩者實(shí)際是有區(qū)別的谆刨。
線程那些事兒
? 互聯(lián)網(wǎng)時代以降塘娶,由于在線用戶數(shù)量的爆炸归斤,單臺服務(wù)器處理的連接也水漲船高,迫使編程模式由從前的串行模式升級到并發(fā)模型血柳,而幾十年來官册,并發(fā)模型也是一代代地升級,有IO多路復(fù)用难捌、多進(jìn)程以及多線程膝宁,這幾種模型都各有長短,現(xiàn)代復(fù)雜的高并發(fā)架構(gòu)大多是幾種模型協(xié)同使用根吁,不同場景應(yīng)用不同模型员淫,揚(yáng)長避短,發(fā)揮服務(wù)器的最大性能击敌,而多線程介返,因?yàn)槠漭p量和易用,成為并發(fā)編程中使用頻率最高的并發(fā)模型沃斤,而后衍生的協(xié)程等其他子產(chǎn)品圣蝎,也都基于它,而我們今天要分析的 goroutine 也是基于線程衡瓶,因此徘公,我們先來聊聊線程的三大模型:
3種模型分別為:
內(nèi)核級線程模型,用戶級線程模型和兩級線程模型哮针,他們之間最大的區(qū)別就在于用戶線程與內(nèi)核調(diào)度實(shí)體(kernel Schedule Entry)的對應(yīng)關(guān)系上关面,而所謂的KSE其實(shí)就是指可以被操作系統(tǒng)內(nèi)核調(diào)度器調(diào)度的對象實(shí)體,其實(shí)就是內(nèi)核級線程十厢,是操作系統(tǒng)內(nèi)核的最小調(diào)度單元等太,也就是我們寫代碼時常說的線程。
Kernel Schedule Entry
首先蛮放,說一下缩抡,什么是KSE?
linux內(nèi)核中實(shí)際沒有線程的概念包颁,linux中的線程缝其,實(shí)際是一種輕量級的進(jìn)程,而進(jìn)程與線程都是KSE徘六。
用戶級線程
用戶級線程中,用戶線程與KSE是N對1的模式榴都,也就是說待锈,所有的用戶線程是在一個進(jìn)程中與一個KSE動態(tài)綁定的,而調(diào)度則都是通過用戶自己的調(diào)度器實(shí)現(xiàn)嘴高,比如說:python的gevent協(xié)程庫就是通過這種方式來實(shí)現(xiàn)的竿音。
而這種實(shí)現(xiàn)有什么好處呢和屎?
所有的行為都在用戶層面解決,CPU對于整個過程是無感的春瞬,避免了用戶態(tài)與內(nèi)核態(tài)來回切換導(dǎo)致的性能消耗柴信。
而他的缺點(diǎn)也很明顯:
其實(shí)這種方式?jīng)]法做到真正意義上的并發(fā):因?yàn)橛脩舻淖哉{(diào)度不存在cpu時鐘中斷和輪轉(zhuǎn)調(diào)度,所以宽气,如果一旦一個用戶線程得到了阻塞調(diào)用随常,所有的用戶線程都會被阻塞。
所以許多這樣實(shí)現(xiàn)的協(xié)程庫將阻塞的行為封裝成非阻塞的行為從而避免這樣的事情萄涯。
內(nèi)核級線程
內(nèi)核級線程中绪氛,用戶線程與KSE是一一對應(yīng)的。而且用戶線程的調(diào)度是交給cpu調(diào)度的涝影。
優(yōu)勢是實(shí)現(xiàn)簡單枣察,直接借助操作系統(tǒng)內(nèi)核的線程以及調(diào)度器,所以CPU可以快速切換調(diào)度線程燃逻,于是多個線程可以同時運(yùn)行序目,因此相較于用戶級線程模型它真正做到了并行處理;但它的劣勢是伯襟,由于直接借助了操作系統(tǒng)內(nèi)核來創(chuàng)建猿涨、銷毀和以及多個線程之間的上下文切換和調(diào)度,因此資源成本大幅上漲逗旁,且對性能影響很大嘿辟。
兩級線程模型
兩級線程模型中,用戶線程與KSE是N對M的關(guān)系片效。
兩級線程模型是根據(jù)前兩個模型綜合而來红伦,既不是完全依賴用戶自調(diào)度,也不是完全依賴cpu調(diào)度淀衣。
所有的用戶線程都會動態(tài)綁定到一個KSE上昙读,然后一旦這個KSE因?yàn)樽枞灰瞥鯿pu時,用戶線程可以繼續(xù)與其他的KSE綁定膨桥。
所以蛮浑,兩級線程模型的調(diào)度是一種中間態(tài)的調(diào)度。即:用戶調(diào)度器實(shí)現(xiàn)從用戶線程到KSE的調(diào)度只嚣,內(nèi)核調(diào)度器實(shí)現(xiàn)從KSE到CPU的調(diào)度沮稚。
G-P-M模型
一般的,對于一個OS線程來說册舞,都會有一個固定大小的內(nèi)存塊(2MB)來做棧儲存上下文信息蕴掏,2MB的大小就顯得十分不靈活,對于簡單的任務(wù),2MB太浪費(fèi)資源盛杰,但對于復(fù)雜的任務(wù)挽荡,2MB又太小了,于是goroutine自己實(shí)現(xiàn)了自己的線程即供。
每個goroutine都有自己的棧定拟,并且采取了動態(tài)擴(kuò)容的方法,初始僅分配2kb的大小逗嫡,并且不斷的動態(tài)擴(kuò)容青自,同時也會有GC來對棧的內(nèi)存進(jìn)行收縮。
任何用戶線程最終肯定都是要交由OS線程來執(zhí)行的祸穷,goroutine(稱為G)也不例外性穿,但是G并不直接綁定OS線程運(yùn)行,而是由Goroutine Scheduler中的 P - Logical Processor (邏輯處理器)來作為兩者的『中介』雷滚,P可以看作是一個抽象的資源或者一個上下文需曾,一個P綁定一個OS線程,在golang的實(shí)現(xiàn)里把OS線程抽象成一個數(shù)據(jù)結(jié)構(gòu):M祈远,G實(shí)際上是由M通過P來進(jìn)行調(diào)度運(yùn)行的呆万,但是在G的層面來看,P提供了G運(yùn)行所需的一切資源和環(huán)境车份,因此在G看來P就是運(yùn)行它的 “CPU”谋减,由 G、P扫沼、M 這三種由Go抽象出來的實(shí)現(xiàn)出爹,最終形成了Go調(diào)度器的基本結(jié)構(gòu):
- G: 表示Goroutine,每個Goroutine對應(yīng)一個G結(jié)構(gòu)體缎除,G存儲Goroutine的運(yùn)行堆棧严就、狀態(tài)以及任務(wù)函數(shù),可重用器罐。G并非執(zhí)行體梢为,每個G需要綁定到P才能被調(diào)度執(zhí)行。
- P: Processor轰坊,表示邏輯處理器铸董, 對G來說,P相當(dāng)于CPU核肴沫,G只有綁定到P(在P的local runq中)才能被調(diào)度粟害。對M來說,P提供了相關(guān)的執(zhí)行環(huán)境(Context)颤芬,如內(nèi)存分配狀態(tài)(mcache)我磁,任務(wù)隊(duì)列(G)等孽文,P的數(shù)量決定了系統(tǒng)內(nèi)最大可并行的G的數(shù)量(前提:物理CPU核數(shù) >= P的數(shù)量),P的數(shù)量由用戶設(shè)置的GOMAXPROCS決定夺艰,但是不論GOMAXPROCS設(shè)置為多大,P的數(shù)量最大為256沉衣。
- M: Machine郁副,OS線程抽象,代表著真正執(zhí)行計(jì)算的資源豌习,在綁定有效的P后存谎,進(jìn)入schedule循環(huán);而schedule循環(huán)的機(jī)制大致是從Global隊(duì)列肥隆、P的Local隊(duì)列以及wait隊(duì)列中獲取G既荚,切換到G的執(zhí)行棧上并執(zhí)行G的函數(shù),調(diào)用goexit做清理工作并回到M栋艳,如此反復(fù)恰聘。M并不保留G狀態(tài),這是G可以跨M調(diào)度的基礎(chǔ)吸占,M的數(shù)量是不定的晴叨,由Go Runtime調(diào)整,為了防止創(chuàng)建過多OS線程導(dǎo)致系統(tǒng)調(diào)度不過來矾屯,目前默認(rèn)最大限制為10000個兼蕊。
G-P-M模型調(diào)度
Go調(diào)度器工作時會維護(hù)兩種用來保存G的任務(wù)隊(duì)列:一種是一個Global任務(wù)隊(duì)列,一種是每個P維護(hù)的Local任務(wù)隊(duì)列件蚕。
當(dāng)通過go
關(guān)鍵字創(chuàng)建一個新的goroutine的時候孙技,它會優(yōu)先被放入P的本地隊(duì)列。為了運(yùn)行g(shù)oroutine排作,M需要持有(綁定)一個P牵啦,接著M會啟動一個OS線程,循環(huán)從P的本地隊(duì)列里取出一個goroutine并執(zhí)行纽绍。當(dāng)然還有上文提及的 work-stealing
調(diào)度算法:當(dāng)M執(zhí)行完了當(dāng)前P的Local隊(duì)列里的所有G后蕾久,P也不會就這么在那躺尸啥都不干,它會先嘗試從Global隊(duì)列尋找G來執(zhí)行拌夏,如果Global隊(duì)列為空僧著,它會隨機(jī)挑選另外一個P,從它的隊(duì)列里中拿走一半的G到自己的隊(duì)列中執(zhí)行障簿。