今天的主角,是Go 面試的萬能題GMP 模型的延伸題(疑問),那就是」GMP 模型,為什麼要有P ?出來,那麼麻煩,為的是什麼,是要解決什麼問題嗎
? 「這篇文章煎魚就帶你一同探索,GM、GMP 模型的變遷是因為什麼原因。
GM 模型static void schedule(G *gp) { ... schedlock(); if(gp != nil) { ... switch(gp->status){ case Grunnable: case Gdead: // Shouldn't have been running! runtime·throw("bad gp->status in sched"); case Grunning: gp->status = Grunnable; gput(gp); break; } gp = nextgandunlock(); gp->readyonstop = 0; gp->status = Grunning; m->curg = gp; gp->m = m; ... runtime·gogo(&gp->sched, 0); }
schedlock
方法來取得全域鎖定。 gput
方法來保存目前 Goroutine 的運行狀態等信息,以便於後續的使用。 nextgandunlock
方法來尋找下一個可運行 Goroutine,並且釋放全域鎖定給其他調度使用。 runtime·gogo
方法,將剛剛所取得的下一個待執行的 Goroutine 運行起來,進入下一輪調度。 透過對Go1.0.1 的調度器原始碼剖析,我們可以發現一個比較有趣的點。那就是調度器本身(schedule 方法),在正常流程下,是不會回傳的,也就是不會結束主流程。
他會不斷地運行調度流程,GoroutineA 完成了,就開始尋找GoroutineB,尋找到B 了,就把已經完成的A 的調度權交給B,讓GoroutineB 開始被調度,也就是運作。
當然了,也有被正在阻塞(Blocked)的 G。假設 G 正在做一些系統、網路調用,那麼就會導致 G 停滯。這時候 M(系統執行緒)就會被會重新放入核心佇列中,等待新的一輪喚醒。
這麼表面的看起來,GM 模型似乎牢不可破,毫無缺陷。但為什麼要改呢?
在2012 年時Dmitry Vyukov 發表了文章《Scalable Go Scheduler Design Doc》,目前也依然是各大研究Go 調度器文章的主要對象,其在文章內講述了整體的原因和考慮,下述內容將引用該文章。
目前(代指 Go1.0 的 GM 模型)的 Goroutine 調度器限制了用 Go 編寫的並發程式的可擴展性,尤其是高吞吐量伺服器和平行計算程式。
實作有以下的問題:
每個 P 有自己的本地佇列,大幅度的減輕了對全域佇列的直接依賴,所帶來的效果就是鎖定競爭的減少。而 GM 模型的效能開銷大頭就是鎖定競爭。
每個P 相對的平衡上,在GMP 模型中也實作了Work Stealing 演算法,如果P 的本機佇列為空,則會從全域佇列或其他P 的本機佇列中竊取可運行的G 來運行,減少空轉,提高了資源利用率。
#這時候就有小夥伴會疑惑了,如果是想實作本地隊列、Work Stealing 演算法,那為什麼不直接在M 上加呢,M 也照樣可以實現類似的功能。
為什麼再加多一個 P 元件?
結合 M(系統執行緒) 的定位來看,若這麼做,有以下問題。
一般來講,M 的數量都會多於 P。像在 Go 中,M 的數量最大限制是 10000,P 的預設數量的 CPU 核數。另外由於 M 的屬性,也就是如果有系統阻塞調用,阻塞了 M,又不夠用的情況下,M 會不斷增加。
M 不斷增加的話,如果本地佇列掛載在 M 上,那就意味著本地佇列也會隨之增加。這顯然是不合理的,因為本地佇列的管理會變得複雜,且 Work Stealing 效能會大幅下降。
M 被系統呼叫阻塞後,我們是期望把他既有未執行的任務分配給其他繼續運行的,而不是一阻塞就導致全部停止。
因此使用 M 是不合理的,那麼引入新的元件 P,把本地佇列關聯到 P 上,就能很好的解決這個問題。
今天這篇文章結合了整個 Go 語言調度器的一些歷史情況、原因分析以及解決方案說明。
」GMP 模型,為什麼要有 P「 這個問題就像是一道系統設計了解,因為現在很多人為了應對面試,會硬背 GMP 模型,或者是泡麵式過了一遍。而理解其中真正背後的原因,才是我們要去學習的去理解。
知其然知其所以然,才可破局。
以上是再見 Go 面試官:GMP 模型,為什麼要有 P?的詳細內容。更多資訊請關注PHP中文網其他相關文章!