淺談Golang的Work Stealing機制
Go的運行時系統(tǒng)使用了一種名為Work Stealing
(工作竊取)的調(diào)度策略來分配Goroutine到可用線程(稱為M
,即Machine)上執(zhí)行。這樣可以最大化CPU使用率,減少任務(wù)調(diào)度的開銷。在這種機制下,任務(wù)隊列和調(diào)度器通過動態(tài)平衡負載來提高并發(fā)性能和吞吐量。
Go的調(diào)度器使用了P(Processor)與M和Goroutine進行交互。每個P都維護了一個本地的Goroutine隊列,新創(chuàng)建的Goroutine首先會被放入創(chuàng)建它的P的本地隊列中。在這個系統(tǒng)中,P可以看作是可調(diào)度Goroutine的數(shù)量,每個P都可以關(guān)聯(lián)一個M來執(zhí)行Goroutine。
Work Stealing 機制的核心思想:每個操作系統(tǒng)線程(M)都有一個本地任務(wù)隊列,它會盡可能地先執(zhí)行自己隊列中的協(xié)程。當(dāng)某個M的P隊列為空,而其他P仍有任務(wù)時,該M會嘗試從其他P中"偷"一些協(xié)程來執(zhí)行,以實現(xiàn)負載均衡
基本工作原理
任務(wù)隊列:每個工作線程(或goroutine)都有自己的雙端隊列(deque),用于存儲任務(wù)。當(dāng)一個線程生成新任務(wù)時,它會將任務(wù)放入自己的隊列。這種隊列就是上述所講的P處理器。
執(zhí)行任務(wù):M首先從自己對應(yīng)P中獲取任務(wù)并執(zhí)行。如果它的任務(wù)隊列為空,它就會嘗試從其他線程的任務(wù)隊列P中竊取任務(wù)。
竊取任務(wù):當(dāng)一個M發(fā)現(xiàn)自己的任務(wù)隊列P為空時,它會隨機選擇其他M的任務(wù)隊列P,從隊列的另一端竊取任務(wù)。這樣可以避免競爭,因為線程對自己的任務(wù)隊列使用一端,而其他線程只能從另一端竊取任務(wù)。
負載均衡:通過這種機制,系統(tǒng)能夠動態(tài)地平衡負載。如果某個線程的任務(wù)較多,其他空閑線程可以幫助處理這些任務(wù),從而避免某些線程過載而其他線程空閑的情況。
全局隊列:如果所有本地隊列P都為空,調(diào)度器會從全局隊列中獲取任務(wù),全局隊列存儲的是所有P都無法處理的goroutine。
以下是一個簡化的示意圖,展示了P, M和Goroutine的交互:
P1 P2 P3 | | | v v v [G1,G2] [G3] [G4,G5,G6] ^ ^ ^ | | | M1 M2 M3
在這個圖中,我們有3個P(P1、P2和P3),每個P都有一個本地的Goroutine隊列。M1、M2和M3是3個線程,每個線程都關(guān)聯(lián)了一個P,并且從其隊列中取出Goroutine來執(zhí)行。當(dāng)M1完成了G1后,它會從P1的隊列中取出G2來執(zhí)行。如果P1的隊列為空,M1就會嘗試從P2或P3的隊列中”竊取”一個Goroutine。
當(dāng)從本線程M 從綁定 P 本地 隊列、全局G隊列、Netpoller 都找不到可執(zhí)行的 G,會從其它 P 里竊取G并放到當(dāng)前P上面
1)如果全局隊列有G,從全局隊列竊取的G數(shù)量:N = min(len(GRQ)/GOMAXPROCS + 1, len(GRQ/2)) (根據(jù)GOMAXPROCS數(shù)量負載均衡)
2)如果 Netpoller 有G(網(wǎng)絡(luò)IO被阻塞的G),從Netpoller竊取的G數(shù)量:N = 1
3)如果從其它P里竊取G,從其它P竊取的G數(shù)量:N = len(LRQ)/2(平分負載均衡)
4)如果嘗試多次一直找不到需要運行的goroutine則進入睡眠狀態(tài),等待被其它工作線程喚醒
從其它P竊取G的源碼見runtime/proc.go stealWork函數(shù),竊取流程如下:
1)選擇要竊取的P
2)從P中偷走一半G
選擇要竊取的P
竊取的實質(zhì)就是遍歷所有P,查看其運行隊列是否有g(shù)oroutine,如果有,則取其一半到當(dāng)前工作線程的運行隊列
為了保證公平性,遍歷P時并不是按照數(shù)組下標(biāo)順序訪問P,而是使用了一種偽隨機的方式遍歷allp中的每個P,防止每次遍歷時使用同樣的順序訪問allp中的元素
offset := uint32(random()) % nprocs coprime := 隨機選取一個小于nprocs且與nprocs互質(zhì)的數(shù) const stealTries = 4 // 最多重試4次 for i := 0; i < stealTries; i++ { // 隨機訪問所有 P for i := 0; i < nprocs; i++ { p := allp[offset] 從p的運行隊列偷取goroutine if 偷取成功 { break } offset += coprime offset = offset % nprocs } }
可以看到只要隨機數(shù)不一樣,遍歷P的順序也不一樣,但可以保證經(jīng)過nprocs次循環(huán),每個P都會被訪問到
從P中偷走一半G
挑選出盜取的對象P之后,則調(diào)用 runtime/proc.go 函數(shù)runqsteal 盜取P的運行隊列中的goroutine,runqsteal函數(shù)再調(diào)用runqgrap從P的本地隊列尾部批量偷走一半的 G
func runqgrab(_p_ *p, batch *[256]guintptr, batchHead uint32, stealRunNextG bool) uint32 { for { h := atomic.LoadAcq(&_p_.runqhead) // load-acquire, synchronize with other consumers t := atomic.LoadAcq(&_p_.runqtail) // load-acquire, synchronize with the producer n := t - h //計算隊列中有多少個goroutine n = n - n/2 //取隊列中g(shù)oroutine個數(shù)的一半 if n == 0 { ...... return ...... } return n } }
優(yōu)點
高效利用資源:通過動態(tài)平衡負載,確保所有線程盡可能地保持忙碌狀態(tài),提高CPU利用率。
減少競爭:竊取任務(wù)時,只訪問其他線程隊列的一端,減少了競爭和鎖的使用。
靈活性:能夠自適應(yīng)負載變化,當(dāng)任務(wù)量不均時,自動進行負載均衡。
到此這篇關(guān)于淺談Golang的Work Stealing機制的文章就介紹到這了,更多相關(guān)Golang的Work Stealing機制內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
在go文件服務(wù)器加入http.StripPrefix的用途介紹
這篇文章主要介紹了在go文件服務(wù)器加入http.StripPrefix的用途介紹,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-12-12golang DNS服務(wù)器的簡單實現(xiàn)操作
這篇文章主要介紹了golang DNS服務(wù)器的簡單實現(xiàn)操作,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2021-04-04Go語言在終端打開實現(xiàn)進度條處理數(shù)據(jù)方法實例
這篇文章主要介紹了Go語言在終端打開實現(xiàn)進度條處理數(shù)據(jù)方法實例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-12-12