線程模型
在細(xì)說 Go 的調(diào)度模型之前,先來說說一般意義的線程模型骨望。線程模型一般分三種硬爆,由用戶級(jí)線程和 OS 線程的不同對(duì)應(yīng)關(guān)系決定的。
N:1擎鸠,即全部用戶線程都映射到一個(gè)OS線程上缀磕,上下文切換成本最低,但無法利用多核資源劣光;
**1:1 **, 一個(gè)用戶線程對(duì)應(yīng)到一個(gè) OS線程上袜蚕, 能利用到多核資源,但是上下文切換成本較高绢涡,這也是 Java Hotspot VM 的默認(rèn)實(shí)現(xiàn)牲剃;
M:N,權(quán)衡上面兩者方案雄可,既能利用多核資源也能盡可能減少上下文切換成本颠黎,但是調(diào)度算法的實(shí)現(xiàn)成本偏高。
為什么 Go Scheduler 需要實(shí)現(xiàn) M:N 的方案滞项?
線程創(chuàng)建開銷大狭归。對(duì)于 OS 線程而言,其很多特性均是操作系統(tǒng)給予的文判,但對(duì)于 Go 程序而言过椎,其中很多特性可能非必要的。這樣一來戏仓,如果是 1:1 的方案疚宇,那么每次** go func(){...} **都需要?jiǎng)?chuàng)建一個(gè) OS 線程,而在創(chuàng)建線程過程中赏殃,OS 線程里某些 Go 用不上的特性會(huì)轉(zhuǎn)化為不必要的性能開銷敷待,不經(jīng)濟(jì)。
減少 Go 垃圾回收的復(fù)雜度仁热。依據(jù)1:1方案榜揖,Go 產(chǎn)生所用用戶級(jí)線程均交由 OS 直接調(diào)度。 Go 的垃圾回收器要求在運(yùn)行時(shí)需要停止所有線程,才能使得內(nèi)存達(dá)到穩(wěn)定一致的狀態(tài)举哟,而 OS 不可能清楚這些思劳,垃圾回收器也不能控制 OS 去阻塞線程。
Go Scheduler 的 M:N 方案出現(xiàn)妨猩,就是為了解決上面的問題潜叛。
Go Scheduler
整個(gè)并發(fā)模型的討論都離不開 Go Scheduler 的設(shè)計(jì)實(shí)現(xiàn)。
首先壶硅,需要了解的是威兜,Go Scheduler 由哪些元素構(gòu)成呢?
M: Machine庐椒,就是 OS 線程本身椒舵,數(shù)量可配置;
P: Processor扼睬, 調(diào)度器的核心處理器逮栅,通常表示執(zhí)行上下文悴势,用于匹配 M 和 G 窗宇。P 的數(shù)量不能超過 **GOMAXPROCS **配置數(shù)量,這個(gè)參數(shù)的默認(rèn)值為CPU核心數(shù)特纤;通常一個(gè) P 可以與多個(gè) M 對(duì)應(yīng)军俊,但同一時(shí)刻,這個(gè) P 只能和其中一個(gè) M 發(fā)生綁定關(guān)系捧存;M 被創(chuàng)建之后需要自行在 P 的 free list 中找到 P 進(jìn)行綁定粪躬,沒有綁定 P 的 M,會(huì)進(jìn)入阻塞態(tài)昔穴。
注:GOMAXPROCS 參數(shù)很重要镰官,其決定了 P 的最大數(shù)量,也決定了自旋 M 的最大數(shù)量吗货。何為自旋泳唠,后面會(huì)提到。
G: Goroutine宙搬,Go 的用戶級(jí)線程笨腥,常說的協(xié)程,真正攜帶代碼執(zhí)行邏輯的部分勇垛,由 **go func(){...} **直接生成脖母;
**G0: **其本身也是 G ,也需要跟具體的 M 結(jié)合才能被執(zhí)行闲孤,只不過他比較特殊谆级,其本身就是一個(gè) schedule 函數(shù),這個(gè)函數(shù)包含如下邏輯:
/src/runtime/proc.go:
func schedule() {
這里涉及到另外幾個(gè)概念,本地隊(duì)列哨苛、全局隊(duì)列以及 “竊取”鸽凶。
本地隊(duì)列(local queue): 本地是相對(duì) P 而言的本地,每個(gè) P 維護(hù)一個(gè)本地隊(duì)列建峭;與 P 綁定的 M 中如若生成新的 G玻侥,一般情況下會(huì)放到 P 的本地隊(duì)列;當(dāng)本地隊(duì)列滿了的時(shí)候亿蒸,才會(huì)截取本地隊(duì)列中 “一半” 的元素放入全局隊(duì)列中凑兰;
全局隊(duì)列(global queue):承載本地隊(duì)列“溢出”的 G。為了保證調(diào)度公平性边锁,schedule 過程中有 1/61 的幾率優(yōu)先檢查全局隊(duì)列姑食,否則本地隊(duì)列一直滿載的情況下,全局隊(duì)列中的 G 將永遠(yuǎn)無法被調(diào)度到茅坛;
竊纫舭搿(stealing): 這似乎和 Java Fork-Join 中的 work-stealing 模型很相似,其目的也是一樣贡蓖,就是為了使得空閑(idle)的 M 有活干曹鸠,不空等,提高計(jì)算資源的利用率。竊取也是有章法的,規(guī)則是隨機(jī)從其他 P 的本地隊(duì)列里竊取 “一半” 的 G恕稠。
綜上,整個(gè)調(diào)度流程就是:
1/61 的幾率在全局隊(duì)列中找 G邻眷,60/61 的幾率在本地隊(duì)列找 G;
如果全局隊(duì)列找不到 G剔交,從 P 的本地隊(duì)列找 G肆饶;
如果找不到,從其他 P 的本地隊(duì)列中竊取 G岖常;
-
如果找不到驯镊,則從全局隊(duì)列中拿取一部分 G 到本地隊(duì)列。這里拿取的 “一部分” 滿足一個(gè)公式:
n = min(len(GQ)/GOMAXPROCS + 1, len(GQ/2))
注:這里 GQ 表示全局隊(duì)列腥椒。
如果找不到阿宅,從網(wǎng)絡(luò)中 poll G。
只要找到了 G笼蛛, 就會(huì)立馬丟給 M 執(zhí)行洒放。當(dāng)然上述任何執(zhí)行邏輯如果沒有 running 的 M 參與,都是無法真正被執(zhí)行的滨砍,這包括調(diào)度邏輯本身往湿。
一言蔽之妖异,調(diào)度的本質(zhì)就是 P 將 G 合理的分配給某個(gè) M 的過程。
線程自旋(Spinning Threads)
線程自旋是相對(duì)于線程阻塞而言的领追,表象就是循環(huán)執(zhí)行一個(gè)指定邏輯(就是上面提到的調(diào)度邏輯他膳,目的是不停地尋找 G)。這樣做的問題顯而易見绒窑,如果 G 遲遲不來棕孙,CPU 會(huì)白白浪費(fèi)在這無意義的計(jì)算上。但好處也很明顯些膨,降低了 M 的上下文切換成本蟀俊,提高了性能。
具體來說订雾,假設(shè)Scheduler 中全局和本地隊(duì)列均為空肢预,M 此時(shí)沒有任何任務(wù)可以處理,那么你會(huì)選擇讓 M 進(jìn)入阻塞狀態(tài)還是選擇讓 CPU 空轉(zhuǎn)等待 G 的駕臨洼哎?
Go 的設(shè)計(jì)者傾向于高性能的并發(fā)表現(xiàn)烫映,選擇了后者。當(dāng)然前面也提到過噩峦,為了避免過多浪費(fèi) CPU 資源锭沟,自旋的線程數(shù)不會(huì)超過 **GOMAXPROCS **,這是因?yàn)橐粋€(gè) P 在同一個(gè)時(shí)刻只能綁定一個(gè) M壕探,P的數(shù)量不會(huì)超過 GOMAXPROCS冈钦,自然被綁定的 M 的數(shù)量也不會(huì)超過郊丛。對(duì)于未被綁定的“游離態(tài)”的 M李请,會(huì)進(jìn)入休眠阻塞態(tài)。
M 如果因?yàn)?G 發(fā)起了系統(tǒng)調(diào)用進(jìn)入了阻塞態(tài)會(huì)怎樣厉熟?
如圖导盅,如果 G8 發(fā)起了阻塞系統(tǒng)調(diào)用(例如阻塞 IO 操作),使得對(duì)應(yīng)的 M2 進(jìn)入了阻塞態(tài)揍瑟。此時(shí)如果沒有任何的處理白翻,Go Scheduler 就會(huì)在這段阻塞的時(shí)間內(nèi),白白缺失了一個(gè) OS 線程單元绢片。
Go 設(shè)計(jì)者的解決方案是滤馍,一旦 G8 發(fā)起 Syscall 使得 M2 進(jìn)入阻塞態(tài),此時(shí)的 P2 會(huì)立即與 M2 解綁底循,保留 M2 與 G8 的關(guān)系巢株,繼而與新的 OS 線程 M5 綁定,繼續(xù)下一輪的調(diào)度熙涤。那么雖然 M2 進(jìn)入了阻塞態(tài)阁苞,但宏觀來看困檩,并沒有缺失任何處理單元,P2 依然正常工作那槽。
那 G8 的阻塞操作返回后怎么辦悼沿?
G8 失去了 P2,意味著失去了執(zhí)行機(jī)會(huì)骚灸,M2 被喚醒以后第一件事就是要竊取一個(gè)上下文(Processor)糟趾,還給 G8 執(zhí)行機(jī)會(huì)。然而現(xiàn)實(shí)是 M2 不一定能夠找到 P 綁定甚牲,不過找不到也沒關(guān)系拉讯,M2 會(huì)將 G8 丟到全局隊(duì)列中,等待調(diào)度鳖藕。
這樣一來 G8 會(huì)被其他的 M 調(diào)度到魔慷,重新獲得執(zhí)行機(jī)會(huì),繼續(xù)執(zhí)行阻塞返回之后的邏輯著恩。
與 Java F-J Task 相比院尔,Goroutine 是否有本質(zhì)區(qū)別?
直覺告訴我喉誊,這兩者并沒有本質(zhì)區(qū)別邀摆。
兩個(gè)方案均為 M:N,兩者定位都是“輕量級(jí)”的用戶線程伍茄,而并非與 OS 線程一一對(duì)應(yīng)栋盹。引入的本地隊(duì)列使得在大多數(shù)場(chǎng)景下,線程爭(zhēng)搶會(huì)更少敷矫。而從調(diào)度算法細(xì)分析來看例获,Go Scheduler 會(huì)比 F-J 更加復(fù)雜,對(duì)線程的控制更加精細(xì)曹仗,其適用的場(chǎng)景也更加全面榨汤。
總之,作為一個(gè) Java 開發(fā)者怎茫,如果你是用 Hotspot VM收壕,是默認(rèn)不支持原生協(xié)程的,但不代表不能實(shí)現(xiàn)所謂的協(xié)程轨蛤,F(xiàn)-J 可以理解為一個(gè)簡(jiǎn)易版實(shí)現(xiàn)蜜宪。
然而 Go 并發(fā)調(diào)度的優(yōu)勢(shì)在哪呢?
原生支持祥山;調(diào)度算法更加科學(xué)圃验,更具備普適性;另外枪蘑,簡(jiǎn)潔的語法掩蓋了底層細(xì)節(jié)损谦,這讓開發(fā)者輕松實(shí)現(xiàn)高性能并發(fā)編程岖免,成為可能。