Golang和Erlang的並發調度淺析

來源:互聯網
上載者:User
這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。

Go 語言和 Erlang 都是面向並發應用的語言,都採用輕量級線程和訊息傳遞模型。儘管Go在文法上也支援共用,但必須以通訊的方式同步方能保證其正確性。Erlang則是完全不支援進程間的共用,狀態資訊完全需要依靠訊息彼此傳遞。

從底層來看,在 Google 官方編譯器中,Go 語言的 Goroutine 是一種類似協程的結構,由於採用了定製的C編譯器來構建,因此其環境切換的效率要高於C庫的 coroutine(只需要切換PC和棧幀,其他寄存器由函數調用者負責儲存); 而在 Go 的 GCC 前端中,Goroutine 則直接由C庫的 coroutine 機制實現。由於 Erlang 是基於 BEAM 虛擬機器執行的,因此它的所謂 “輕量進程” 也就僅僅是 BEAM 上的概念,不對應C語言或OS級的概念。

從調度策略來看,Go 完全是協作式調度,一個執行中的 Goroutine 僅在操作被阻塞或顯示讓出處理器時被切換出去,Goroutine之間也沒有優先順序之分; Erlang 則採用一種名為“Reduction-Counting”的輪轉調度策略,並且存在4個進程優先順序。

值得注意的是在 Go 1.2 版之後,增加了一些簡單的搶佔機制,但僅有使用者程式函數調用時刻才可能觸發搶佔的判斷,並不是真正意義上的搶佔,具體思想參見這裡。

Go 的調度器的最新版實現了M:N的調度方式,通過 GOMAXPROCS 指定最大的並行能力; Erlang 的 BEAM 虛擬機器也支援SMP方式,一般情況下以系統的核心數或硬體執行緒數作為其調度器個數,每個調度器會綁定到一個OS線程,IO 等阻塞型操作由單獨的系統線程負責調度。

Go 的Server Load Balancer一般是採用 “Work-Stealing” 方式;Erlang則是維護一個“任務遷移隊列”,調度器會定期計算任務遷移的路徑。此外,Erlang也提供了“Work-Stealing” 方式作為補充。充。

Go的調度模型簡介

對於線程調度器,一般有3中模型:

  • N:1,即多個使用者線程運行在一個OS線程上
  • 1:1,即使用者線程和OS線程一一對應
  • N:M,即一定數量的使用者線程映射到一定數量的OS線程上

第一種方式的優點是使用者線程切換較快,但可擴充性不好,難以很好發揮多核處理器的並行性(libtask 屬於該類型); 而第二種與之相反,其能很好的利用多核並行性,但是使用者線程資源開銷和調度成本都比較大。 第三種方式理論上能在調度開銷和並行性之間取得較好的折衷。

在Go 1.1 中,Dmirty Vyukov 對調度器進行了重新設計,由原來的 1:1 模型進化到 M:N 模型,從而使 Go 在並行編程效能上有了顯著的提升。

Go 的新調度器模型主要涉及3個核心概念:M、P及G,如所示:

M 代表OS的線程,P代表當前系統的處理器數(一般由GOMAXPROCS 環境變數指定),G代表Go語言的使用者級線程,也就是通常所說的 Goroutine。

新的調度器由1:1 進化到 M:N 的關鍵在於新加了 P 這個抽象結構。在多核平台上,P的數量一般對應處理器核心或硬體執行緒的數量,調度器需要保證所有的P都有G執行,以保證並行度。

M 必須與P綁定方能執行任務G,如所示:

在舊版 Go 調度器實現中,由於缺少P, 一旦運行 G (goroutine)的 M (OS線程)陷入阻塞狀態(如調用某個阻塞的系統調用)時,M 對應的 OS 線程就會被作業系統調度出去,從而導致系統中其他就緒的G也不能執行;而添加了P這個邏輯結構後,一旦發生上述情況,阻塞的 M 將被與其對應的 P 剝離,RUNTIME會再分配一個 M 並將其與已經剝離出來的 P 綁定,運行其他就緒的G。這個過程如所示:

在實際實現中,考慮到代碼執行的局部性因素,一般會傾向於延遲 M 與 P 剝離的時機。具體來說,RUNTIME中存在一個駐留的線程sysmon,用來監控所有進入Syscall 的 M,只有當 Syscall 執行時間超出某個閾值時,才會將 M 與 P 分離。

另外一個保證系統運行穩定性的方式是負載平衡機制,在Go中,用了 “任務竊取” 的方法。

首先介紹一下 Go 的任務隊列,每個 P 都有一個私人的任務隊列 (實現上是一個用數組表示的迴圈鏈表)以及一個公用隊列(單鏈表表示),私人隊列的功能是為了減輕公用隊列的競爭開銷。

當一個 P 的私人任務隊列為空白時,它會從全域隊列中尋找就緒態的 G 執行;如果全域隊列也為空白,則會隨機播放竊取其他 P 私人執行隊列中的任務G,從而保證所有線程儘可能以最大負載工作。其如下:

由於 P 的私人隊列採用了數組結構,很容易計算出隊列中間的位置,因此“竊取者” 採用了與 “被竊取者” 均分任務的方法,以儘可能達到負載平衡。

無論從公用隊列取任務還是進行“竊取”,都會引起一定的競爭開銷,因此 RUNTIME 會傾向於將建立任務或新轉變為就緒態的任務添加到當前執行 P 的私人隊列中。 僅當執行的任務調用 yield 機制讓出處理器或進入了一個長時間執行的系統調用時,該任務才會被添加到公用隊列中。

以上關於Go調度器的部分內容及圖片轉自:http://morsmachine.dk/go-scheduler

Erlang的調度模型簡介

由於 Erlang 程式是運行在 BEAM 虛擬機器之上,因此其調度器在實現上和 Go 等 Native 語言存在較大的差異,但其內部涉及的基本原理都是類似的,可以互相參考。

早期的 BEAM 虛擬機器是單線程啟動並執行,直到2006年才引入了 SMP 版本的 BEAM 虛擬機器,經過了若干早期版本的演化,逐漸形成了今天的版本。最新版本的Erlang可以通過命令列參數指定是否啟用 SMP 版本虛擬機器。

BEAM 上的調度單位是“輕量進程”,這是一種虛擬機器上的輕量級執行線索(由於 Erlang 的 process 是不共用記憶體的,行為更像進程而非線程,因此我們在這裡叫它“輕量進程”)。每個 Erlang 進程包括一個控制塊(PCB)、一個棧和私人的堆空間,一些特殊的結構,如位元據,ETS 表是進程間共用的,使用全域堆空間。

BEAM 虛擬機器裡存在一些並行的調度器,一般情況下,一個調度器會映射為一個 OS 線程,這種方式類似於早期的Go語言實現(只有M和G,沒有P),每個調度器擁有各自的任務隊列,調度器之間的Server Load Balancer通過引入專門的任務遷移機製得以實現。其原理如所示:

通常,調度器的數量與運行平台的處理器核心數或硬體執行緒數相等,也可以通過 BEAM 命令列參數指定,或在運行時動態修改。

在BEAM系統中,除了process之外,還存在三種其他的調度單位:連接埠(ports)、鏈入式驅動(linkd-in drivers)和系統級活動(system level activities); 這三種特殊的任務形式主要用來進行IO操作和執行其他語言的代碼等功能,其部分功能很像 Go 中對執行阻塞 Syscall 任務的“剝離”機制,具體實現方法這裡暫時不討論。我們主要將精力集中在 Erlang 的 process 的調度機制上。

與 Go 不同,Erlang 的調度器是一個輪轉而非協作式的調度器,每個進程建立時會被分配一個稱為“reduction”的值,是一個計算量的度量(基本上等同於函數調用的次數),類似 OS 的時間片。進程每執行一定量的計算後,reduction值就會累計,一旦達到閾值,該進程就會給切換出去。這種調度方式在 Erlang 中被稱為 “reduction-counting”。

採用輪轉的調度方式能更好的防止程式設計不當而導致的個別進程餓死的情況,同時能夠實現更好的即時性功能。

同時,Erlang還為進程提供了四個不同的優先順序:max,high,normal和low。不同優先順序進程按優先順序調度;同級進程按輪轉方式調度。每個調度器包含3個任務隊列,Max和High具有單獨的隊列,normal和low則位於同一個隊列 —— 調度器忽略一定次數的low級進程來實現二者間的差別。

Erlang 調度器之所以能夠實現優先順序輪轉調度,主要是得益於其基於虛擬機器的執行方式:由於每條Erlang指令都需要經過 BEAM 解釋執行,因此 process 的運行完全處於BEAM的監控之下,BEAM可以方便的完成對進程的切換。與之相對,由於 Go 的 Goroutine 與 RUNTIME 都是 Native 執行的,其在執行上的地位是平等的,RUNTIME 沒有能力切換一個執行中的 Goroutine,除非其自己調出或調用RUNTIME 功在 ,因而只能實現協作調度。

註: Go 1.2 中,添加了簡單的“使用者態”任務搶佔機制,主要是在系統線程sysmon中監控Goroutine的執行時間,然後藉助“動態棧擴充”機制,在函數調用時刻切入RUNTIME並實現搶佔。這種方式雖然很巧妙,但對某些特殊的情況,如沒有調用非inline函數的耗時計算等,就沒有多大效果力了。

Erlang 調度器通過定期進行“任務遷移”來達到Server Load Balancer。“任務遷移”過程在同一時刻只能由一個調度器發起。首先,根據各調度器的任務隊列的長度計算一個叫“Migtation limit”的值,這個值就是各調度器就緒隊列長度的均值;然後,開始計算“Migataion Path”,演算法是:

  1. 計算各隊列長度與“Migtation limit”差值
  2. 找到差值中正最大和負最小的隊列,記錄一個從前者到後者進行任務遷移路徑,以達到二者都接近“Migtation limit”
  3. 重複步驟1,直到達到負載平衡

顯示了上述演算法的執行個體:

“Migatation Path” 計算完成後,在每個調度時刻,調度器都會檢查該路徑,根據其指導去抓取(pull)或推送(push)相應任務隊列的任務。這一步驟完成了真正的負載平衡。

作為“任務遷移”機制的補充,Erlang調度器還支援“任務竊取”機制:當一個活躍的調度器自己的任務隊列為空白且不能通過“任務遷移路徑”抓取任務時,它會主動竊取其他調度器任務隊列上的就緒任務,如果仍然沒有可供執行的任務,則該調度器進入Waiting狀態。

關於Erlang調度模型,主要部分參考了這篇文章的第三章及Erlang/OTP源碼。

結論

通過上述簡單對比,我們大體上瞭解了Erlang和Go兩種語言在並發任務調度上的異同,可以說二者各有優缺點:Go 的調度模型更加高效(Native)而 Erlang 則提供了更強大的功能(即時性、優先順序)。

關於調度器,其實還有很多內容,如 Go 和 Erlang 都支援“記憶體回收”,而GC在兩種語言中對調度的影響如何等;同時,講Go的 M:N 調度時說到 M 一旦陷入Syscall 阻塞後,系統會建立一個新的M(OS 線程)來接管 P 及其任務隊列,那麼當設計一個高度並發的IO系統時(如 Web 服務器),頻繁的Syscall會導致大量 OS 線程建立,從而影響效能。Go如何解決這個問題呢?

在後續分析中,會針對 IO 和 GC 部分進行更加深入的討論,以解答餘下的有關調度器的問題。

特別說明: 由於Go語言正處於高速發展的階段,因此一些現在分析的內容可能會隨時更新,在本文完成時, 其穩定版本是 1.2 , 而包含大量更新的 1.3 版也呼之欲出,因此若本文內容不免出現滯後或錯誤,請大家及時指正!

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.