這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
原文連結:Go's work-stealing scheduler
Go 發送器的任務是在多個運行在一個或多個處理器上的系統線程上分發出可啟動並執行 goroutine。在多線程計算中,調度已經出現了兩種模式:工作共用與工作竊取。
- 工作共用:當一個處理器建立新的線程時,它試圖將一部分線程遷移到其他的處理器上執行,期望更充分的利用那些 IDLE 狀態的處理器。
- 工作竊取:未被充分利用的處理器會主動尋找其他處理器上的線程,並“竊取”一些線程。
與工作共用模式相比,工作竊模數式線程的遷移發生的頻率更低。當所有的處理器都有工作要運行時,沒有任何線程被遷移。一旦有了空閑處理器,就會考慮遷移。
Go 從 1.1 版本開始就有了一個工作竊模數式的發送器,它是由 Dmitry Vyukov 貢獻的。這篇文章將深入地解釋什麼是工作竊取的發送器,以及如何?一個。
調度的基礎
Go有一個 M:N 的發送器,它可以使用多核處理器。在任何時候,M 個 goroutine 都需要被分發在最多 GOMAXPROCS 個處理器上啟動並執行 N 個系統線程上。Go發送器使用以下術語來描述 goroutines、線程和處理器:
- G:goroutine
- M:系統線程(Machine)
- P:處理器
有一個特定於處理器的本地 goroutine 隊列和一個全域的 goroutine 隊列。每個系統線程都應該被分配給一個處理器,如果處理器被阻塞或被系統調用,那麼可能處理器上會沒有線程。在任何時候,最多有
GOMAXPROCS 個處理器被用於分配。任何時候,一個線程都只能運行在一個處理器上。如果有需要調度器也可以建立更多的線程。
每一輪的調度都只是找到一個可啟動並執行 goroutine 並執行它。在每一輪的調度中,搜尋都按照以下順序進行:
runtime.schedule() { // only 1/61 of the time, check the global runnable queue for a G. // if not found, check the local queue. // if not found, // try to steal from other Ps. // if not, check the global runnable queue. // if not found, poll network.}
一旦找到一個可啟動並執行 goroutine ,它就會被執行,直到被阻塞。
注意:看起來全域隊列比局部隊列優先順序更高,但是偶爾檢查全域隊列只是為了避免系統線程在局部隊列中的 goroutine 用盡前只調用局部 goroutine 隊列中的 goroutine。
竊取
當一個 goroutine 被建立或一個已經存在的 goroutine 變為可以運行狀態,它被推送到當前處理器上的一個可啟動並執行 goroutines 隊列中,當處理器執行完一個 goroutine,它將試圖從自己的局部可運行 goroutine 隊列中彈出這個 goroutine。如果隊列為空白,處理器會隨機播放一個其他處理器,並試圖從這個處理器的局部可運行 goroutine 隊列中偷取一半數量的可運行 goroutine。
在上面的例子中,P2 這個處理器無法找到任何可執行檔 goroutines。因此,它隨機播放另一個處理器 P1,並將 3 個 goroutines 偷到自己的局部隊列中。P2 將執行這些 goroutines,而調度器也將在多個處理器之間更均衡的調度。
旋轉線程
發送器總是希望將可啟動並執行 goroutines 分發到線程中,以便充分利用處理器,但同時我們又需要限制過多的任務來節省 CPU 資源。與此相矛盾的是,調度器還需要能夠擴充到高輸送量和CPU密集的程式中。
如果效能很關鍵,那麼經常性的搶佔將顯得十分的昂貴,並且對高輸送量的程式來說這也是個嚴重的問題。作業系統線程之間不應該頻繁的傳遞可啟動並執行 goroutine,因為這將導致延遲的增加。另外,在有系統調用的情況下,系統線程需要不斷的 block 與 unblock,這也是非常昂貴的同時也增加了很多額外的開銷。
為了減少線程間傳遞,調度器實現了“旋轉線程”。旋轉線程雖然消耗了額外的 CPU 資源,但是它們最小化了作業系統線程的搶佔。一個線程正在旋轉,如果:
- 一個擁有線程的處理器正在尋找一個可執行檔 goroutine。
- 一個沒有所屬處理器的線程正在尋找一個可以依附的處理器
- 當它準備一個 goroutine 時,如果有一個閒置處理器並且沒有其他的旋轉線程,發送器會取消一個額外的線程,然後旋轉這個線程。
在任何時候,都有最多 GOMAXPROCS 個線程在旋轉。當一個旋轉的線程找到工作後,它就會脫離自旋狀態。
如果閒置線程沒有處理器可分配,則已指派到處理器上的空閑線程不會阻塞。當新的 goroutine 被建立或者一個線程被阻塞時,發送器將確保至少有一個旋轉的線程,這確保了當沒有可啟動並執行 goroutine 時程式仍可以運行,也避免過多的線程 block/unblock。
Go發送器做了很多工作,以避免對作業系統線程的過度搶佔,通過將它們調度到正確的和未充分利用的處理器上,並實現了“旋轉”線程,以避免出現阻塞/未阻塞的轉換。
調度事件可以通過執行跟蹤程式跟蹤。如果你認為你的處理器利用率很低,那麼你可以排查一下正在發生什麼。