1. 程式人生 > >【轉】Go調度器原理淺析

【轉】Go調度器原理淺析

返回 tab thread 其中 時代 使用 hat roc 思想

goroutine是golang的一大特色,或者可以說是最大的特色吧(據我了解),這篇文章主要翻譯自Morsing的[這篇博客](http://morsmachine.dk/go-scheduler),我讀這篇文章的時候不只是贊嘆調度器設計的精巧,而且被Unix內核設計思想的影響和輻射所震撼,感覺好多好東西都帶著它的影子。

緒論(Introduction)
---------------------
Go 1.1最大的特色之一就是這個新的調度器,由Dmitry Vyukov貢獻。新調度器讓並行的Go程序獲得了一個動態的性能增長,針對它我不能再做點更好的工作了,我覺得我還是為它寫點什麽吧。

這篇博客裏面大多數東西都已經被包含在了[原始設計文檔](https://docs.google.com/document/d/1TTj4T2JO42uD5ID9e89oa0sLKhJYD0Y_kqxDv3I3XMw)中了,這個文檔的內容相當廣泛,但是過於技術化了。



關於新調度器,你所需要知道的都在那個設計文檔中,但是我這篇博客有圖片,所以更加清晰易懂。

帶調度器的Go runtime需要什麽?(What does the Go runtime need with a scheduler?)
-------------------------------------------------------------------------------
但是在我們開始看新調度器之前,我們需要理解為什麽需要調度器。為什麽既然操作系統能為我們調度線程了,我們又創造了一個用戶空間調度器?

POSIX線程API是對現有Unix進程模型的一個非常大的邏輯擴展,而且線程獲得了非常多的跟進程相同的控制。比如,線程有它自己的信號掩碼,線程能夠被賦予CPU affinity功能(就是指定線程只能在某個CPU上運行),線程能被添加到[cgroups](http://en.wikipedia.org/wiki/Cgroup)中,線程所用到的資源也可以被查詢到。所有的這些控制增大了Go程序使用gorroutines時根本不需要的特性(features)的開銷,當你的程序有100,000個線程的時候,這些開銷會急劇增長。


另外一個問題是,基於Go模型,操作系統不能給出特別好的決策。比如,當運行一次垃圾收集的時候,Go的垃圾收集器要求所有線程都被停止而且要求內存要處於一致狀態(consistent state)。這個涉及到要等待全部運行時線程(running threads)到達一個點(point),我們事先知道在這個地方內存是一致的。


當很多被調度的線程分散在隨機的點(random point)上的時候,結果就是你不得不等待他們中的大多數到達一致狀態。Go調度器能夠作出這樣的決策,就是只在內存保持一致的點上進行調度。這就意味著,當我們為垃圾收集而停止的時候,我們只須等待在一個CPU核(CPU core)上處於活躍運行狀態的線程即可。



來看看裏面的各個角色(Our Cast of Characters)
-----------------------------------------
目前有三個常見的線程模型。一個是N:1的,即多個用戶空間線程運行在一個OS線程上。這個模型可以很快的進行上下文切換,但是不能利用多核系統(multi-core systems)的優勢。另一個模型是1:1的,即可執行程序的一個線程匹配一個OS線程。這個模型能夠利用機器上的所有核心的優勢,但是上下文切換非常慢,因為它不得不陷入OS(trap through the OS)。

Go試圖通過M:N的調度器去獲取這兩個世界的全部優勢。它在任意數目的OS線程上調用任意數目的goroutines。你可以快速進行上下文切換,並且還能利用你系統上所有的核心的優勢。這個模型主要的缺點是它增加了調度器的復雜性。

為了完成調度任務,Go調度器使用了三個實體:

技術分享圖片


三角形表示OS線程,它是由OS管理的可執行程序的一個線程,而且工作起來特別像你的標準POSIX線程。在運行時代碼裏,它被成為M,即機器(machine)。

圓形表示一個goroutine。它包括棧、指令指針以及對於調用goroutines很重要的其它信息,比如阻塞它的任何channel。在可執行代碼裏,它被稱為G

矩形表示用於調用的上下文。你可以把它看作在一個單線程上運行代碼的調度器的一個本地化版本。它是讓我們從N:1調度器轉到M:N調度器的重要部分。在運行時代碼裏,它被叫做P,即處理器(processor)。這部分後面會多說點。

技術分享圖片


我們可以從上面的圖裏看到兩個線程(M),每個線程都擁有一個上下文(P),每個線程都正在運行一個goroutine(G)。為了運行goroutines,一個線程必須擁有一個上下文

上下文的數目在啟動時被設置為環境變量GOMAXPROCS的值或者通過運行時函數GOMAXPROCS()來設置。通常,在你的程序執行時它不會發生變化。上下文的數目被固定的意思是,只有GOMAXPROCS個上下文正在任意點上運行Go代碼。我們可以使用GOMAXPROCS調整Go進程的調用使其適合於一個單獨的計算機,比如一個4核的PC中可以在4個線程上運行Go代碼。

外部的灰色goroutines沒在運行,但是已經準備好被調度了。它們被安排成一個叫做runqueue的列表。當一個goroutine執行一個go 語句的時候,goroutine就被添加到runqueue的末端。一旦一個上下文已經運行一個goroutine到了一個點上,它就會把一個goroutine從它的runqueue給pop出來,設置棧和指令指針並且開始運行這個goroutine

為了降低mutex競爭,每一個上下文都有它自己的runqueue。Go調度器曾經的一個版本只有一個通過mutex來保護的全局runqueue,線程們經常被阻塞來等待mutex被解除阻塞。當你有許多32核的機器而且想盡可能地壓榨它們的性能時,情況就會變得相當壞。

只要所有的上下文都有goroutines要運行,調度器就能在一個穩定的狀態下保持調度。但是有幾個你能改變的場景。

你打算(系統)調用誰?(Who you gonna (sys)call?)
------------------------------------------------------
你現在可能想知道,為什麽一定要有上下文?我們能不能丟掉上下文而僅僅把runqueue放到線程上?不盡然。`我們用上下文的原因是如果正在運行的線程因為某種原因需要阻塞的時候,我們可以把這些上下文移交給其它線程`。

我們需要阻塞的一個例子是,當我們需要調用一個系統調用的時候。因為一個線程不能既執行代碼同時又阻塞到一個系統調用上,我們需要移交對應於這個線程的上下文以讓這個上下文保持調度。

技術分享圖片


從上圖我們能夠看出,一個線程放棄了它的上下文以讓另外的線程可以運行它。調度器確保有足夠的線程來運行所有的上下文。上圖中的M1 可能僅僅為了讓它處理圖中的系統調用而被創建出來,或者它可能來自一個線程池(thread cache)。這個處於系統調用中的線程將會保持在這個導致系統調用的goroutine上,因為從技術上來說,它仍然在執行,雖然阻塞在OS裏了。

當這個系統調用返回的時候,這個線程必須嘗試獲取一個上下文來運行這個返回的goroutine,操作的正常模式是從其它所有線程中的其中一個線程中“偷”一個上下文。如果“偷盜”不成功,它就會把它的goroutine放到一個全局runqueue中,然後把自己放到線程池中或者轉入睡眠狀態。

這個全局runqueue是各個上下文在運行完自己的本地runqueue後用來獲取新goroutine的地方。上下文也會周期性的檢查這個全局runqueue上的goroutine,否則,全局runqueue上的goroutines可能得不到執行而餓死。

`Go程序要在多線程上運行的原因就是因為要處理系統調用,哪怕GOMAXPROCS等於1`。運行時(runtime)使用調用系統調用的goroutines,而不是線程。

盜取工作(Stealing work)
-----------------------------
系統的穩定狀態改變的另外一個方法是,當一個上下文運行完要被調度的所有goroutines的時候。如果各個上下文的runqueue裏的工作的數目不均衡,改變就會發生了,否則會導致一個上下文在執行完它的runqueue後就會結束,盡管系統中仍然有許多工作要執行。所以為了保持運行Go代碼,一個上下文能夠從全局runqueue中獲取goroutines,但是如果全局runqueue中也沒有goroutines了,那麽上下文就不得不從其它地方獲取goroutines了。

技術分享圖片


這個“其它地方”指的是其它上下文!當一個上下文完成自己的任務後,它就會嘗試“盜取”另一個上下文runqueue中工作量的一半。這將確保每個上下文總是有活幹,然後反過來確保所有線程盡可能處於最大負荷。

下一步走向何方?(Where to go?)
--------------------------------------
關於調度器還有許多細節,像cgo線程、LockOSThread()函數以及與網絡poller的整合。這些已經超過這篇文章的要探討的範圍了,但是仍然值得去研究。以後我會針對這些再寫點文章。在Go運行時庫裏,仍然有大量有意思的創建工作要做。

By Daniel Morsing

(end)

轉載地址:https://www.douban.com/note/300631999/

【轉】Go調度器原理淺析