Erlang調度器細節探析

Erlang調度器細節探析

Erlang的不少基礎特性使得它成爲一個軟實時的平臺。其中包括垃圾回收機制,詳細內容能夠參見個人上一篇文章Erlang Garbage Collection Details and Why It Mattershtml

什麼是調度

通常來講,調度是一種將工做分配給工做者的機制。這些工做能夠是數學運算,字符串處理,數據提取,工做者指的是相似於Green Threads或者原生線程等這種資源。調度器就是執行調度任務的程序,它在某種程度上提供:最大化吞吐,公平執行,最小化響應時間和最小化延時。調度是多任務操做系統/虛擬機的主要部分。它分爲兩種:node

  1. 搶佔式:搶佔式調度器在全部運行任務中切換上下文,而且有權利搶佔(中斷)任務執行並稍後恢復執行而不須要被強佔的任務配合。它基於優先級,時間切片,reduction技術。git

  2. 協做式:協做式調度器在進行上下文切換時須要任務的配合。在這種調度模式下調度器讓運行的任務週期性主動釋放控制權或者在idle狀態時主動釋放,而後開始執行新任務,等待新任務自發返回控制權。github

如今的問題是哪一種調度方式適合必須在限定時間內響應的實時系統。協做式調度不能知足要求,由於實時系統中的運行任務可能永遠不會在限定時間內主動釋放控制權或者返回。因此實時系統一般使用搶佔式調度。api

Erlang調度

Erlang做爲實時多任務平臺,它使用搶佔式調度。Erlang調度器的職責是選擇一個Process而後執行它的代碼。它也負責垃圾回收和內存管理。如何選擇process取決於它們的優先級,每一個process的優先級都是可配置的。對於每一個優先級,多個process輪詢調度。另外一方面,搶佔一個process取決於它最後一次執行到目前的肯定數目的reductions操做,而無論優先級。reductions是每一個線程的計數器,若是有函數調用就增長計數。當該計數器到達max reduction,調度器就會搶佔process並切換上下文。在Erlang/OTP R12B中,max reduction2000數據結構

Erlang調度機制有很長的歷史,歷經數次改變。這些改變也受Erlang中對稱多線程(SMP)特性的影響。多線程

Erlang R11B以前的調度

R11B版本以前,Erlang不支持SMP,只有一個調度器運行在OS進程中的線程,也只有一個Run Queue。調度器從run queue中選擇可運行的process或I/O任務執行。併發

Erlang 虛擬機
+--------------------------------------------------------+
|                                                        |
|  +-----------------+              +-----------------+  |
|  |                 |              |                 |  |
|  |    Scheduler    +-------------->     Task # 1    |  |
|  |                 |              |                 |  |
|  +-----------------+              |     Task # 2    |  |
|                                   |                 |  |
|                                   |     Task # 3    |  |
|                                   |                 |  |
|                                   |     Task # 4    |  |
|                                   |                 |  |
|                                   |     Task # N    |  |
|                                   |                 |  |
|                                   +-----------------+  |
|                                   |                 |  |
|                                   |    Run Queue    |  |
|                                   |                 |  |
|                                   +-----------------+  |
|                                                        |
+--------------------------------------------------------+

這種實現不須要鎖數據結構可是老舊代碼不能享受新處理器並行快餐。函數

Erlang R11B/R12B 的調度

在這兩個版本中因爲SMP的加入,OS進程的一個線程能夠運行1-1024個調度器。然而,這個版本的調度器從公共run queue選擇可運行任務而不像以前那樣只有一個run queue性能

Erlang 虛擬機
+--------------------------------------------------------+
|                                                        |
|  +-----------------+              +-----------------+  |
|  |                 |              |                 |  |
|  |  Scheduler # 1  +-------------->     Task # 1    |  |
|  |                 |    +--------->                 |  |
|  +-----------------+    |    +---->     Task # 2    |  |
|                         |    |    |                 |  |
|  +-----------------+    |    |    |     Task # 3    |  |
|  |                 |    |    |    |                 |  |
|  |  Scheduler # 2  +----+    |    |     Task # 4    |  |
|  |                 |         |    |                 |  |
|  +-----------------+         |    |     Task # N    |  |
|                              |    |                 |  |
|  +-----------------+         |    +-----------------+  |
|  |                 |         |    |                 |  |
|  |  Scheduler # N  +---------+    |    Run Queue    |  |
|  |                 |              |                 |  |
|  +-----------------+              +-----------------+  |
|                                                        |
+--------------------------------------------------------+

因爲並行的加入,全部的共享數據結構都被鎖保護。run queue它自身是一個共享數據結構,必須鎖住。雖然鎖會形成性能懲罰(performance penalty),可是在多核處理器上運行性能有所提高。

這個版本有一些已知的性能瓶頸:

  • 當調度器數目增長時公共run queue會成爲一個瓶頸
  • 對涉及鎖的ETS tables操做會影響Mnesia
  • 當不少process向一個process發送消息會增長鎖衝突概率
  • process等待鎖會阻塞它的調度器

然而,在下個版本能夠看到,爲每一個調度器建立一個run queue解決了上述問題。

Erlang R13B 的調度

在這個版本中每一個調度器有一個run queue。它大大下降了多核系統上鎖衝突的概率,也提升了整體的性能

Erlang虛擬機
+--------------------------------------------------------+
|                                                        |
|  +-----------------+-----------------+                 |
|  |                 |                 |                 |
|  |  Scheduler # 1  |  Run Queue # 1  <--+              |
|  |                 |                 |  |              |
|  +-----------------+-----------------+  |              |
|                                         |              |
|  +-----------------+-----------------+  |              |
|  |                 |                 |  |              |
|  |  Scheduler # 2  |  Run Queue # 2  <----> Migration  |
|  |                 |                 |  |     Logic    |
|  +-----------------+-----------------+  |              |
|                                         |              |
|  +-----------------+-----------------+  |              |
|  |                 |                 |  |              |
|  |  Scheduler # N  |  Run Queue # N  <--+              |
|  |                 |                 |                 |
|  +-----------------+-----------------+                 |
|                                                        |
+--------------------------------------------------------+

如今訪問run queue致使鎖衝突的概率大大下降,但也引入了新議題:

  • run queue的任務劃分對於process來講公平嗎
  • 若是一個process超負荷另外一個idle怎麼辦
  • 調度器應該基於什麼順序來將超負荷的任務轉移
  • 若是咱們運行不少調度器可是隻有少許任務怎麼辦
    這些人們關心的議題使得Erlang開發團隊引入新概念使得調度公平高效,即Migration Logic。它基於以前蒐集的統計信息來控制run queue任務數,使其保持相對平衡。

然而,咱們不該該依賴於調度控制run queue,由於極可能後續版本會有所改變。

控制和監控API

這裏是一些Erlang模擬器的flag,它也能夠控制/監控虛擬機內部調度行爲。

  • 調度線程

在啓動erlang模擬器時,能夠經過flag傳遞兩個由冒號(:)分離的數字來指定

$ erl +S MaxAvailableSchedulers:OnlineSchedulers

最大可用調度線程數只能在啓動時指定,但online調度線程數既能夠在啓動時指定也能夠在運行時改變。
好比,咱們能夠啓動16個可用調度線程,8個online調度線程。

$ erl +S 16:8

而後像下面同樣調用函數改變online線程數目

> erlang:system_info(schedulers). %% => returns 16
> erlang:system_info(schedulers_online). %% => returns 8
> erlang:system_flag(schedulers_online, 16). %% => returns 8
> erlang:system_info(schedulers_online). %% => returns 16

另外,使用+SPflag能夠按百分比設置。

  • process優先級

正如我以前說的,調度器選擇process執行取決於優先級,這個優先級能夠由erlang:process_flag/2指定

PID = spawn(fun() ->
   process_flag(priority, high),
   %% ...
   end).

優先級能夠是low | normal | high | max之一。默認優先級是normalmax爲erlang運行時保留,用戶不該該使用它。

  • run queue信息統計

以前說到run queue存放能夠執行的process,等待調度器選擇。如今能夠調用erlang:statistics(run_queue)獲取run queue中全部能夠執行的process的數目。舉個實際的例子,咱們啓動erlang模擬器,指定4個online調度線程,分配10個CPU密集的process併發執行,任務能夠考慮計算素數個數

%% Everything is clean and ready
> erlang:statistics(online_schedulers). %% => 4
> erlang:statistics(run_queue). %% => 0

%% Spawn 10 heavy number crunching processes concurrently
> [spawn(fun() -> calc:prime_numbers(10000000) end) || _ <- lists:seq(1, 10)].

%% Run queues have remaining tasks to do
> erlang:statistics(run_queue). %% => 8

%% Erlang is still responsive, great!
> calc:prime_numbers(10). %% => [2, 3, 5, 7]

%% Wait a moment
> erlang:statistics(run_queue). %% => 4

%% Wait a moment
> erlang:statistics(run_queue). %% => 0

由於併發process比online調度線程多,調度器會花上較多時間執行全部process直到run queue爲空。有趣的是在spawn這些CPU密集process後,因爲搶佔式調度,erlang模擬器一直保持響應。它不會讓這些流氓process消耗全部運行時,而讓其它可能輕量級但很重要的process餓死,這對於實時系統來講是很是棒的一個特性。

總結

雖然實現一個搶佔式調度系統很複雜,但萬幸這不是開發者的事,它內置於erlang虛擬機。另外一方面,對於一個全部process資源須要相對公平,響應時間不能太長的實時系統來講,額外的跟蹤,平衡,選擇,搶佔線程的成本是徹底能夠接受的。還有,徹底搶佔調度須要操做系統的支持,但就平臺或者庫的角度上,Erlang虛擬機能夠說是最獨特的那個:JVM線程依賴於操做系統調度器,CAF,一個基於actor模型的C++庫,使用協做式調度。Golang不是徹底搶佔式,Python的Twisted也不是,Ruby的event machine和nodejs一樣也不是。這不是說erlang老是最好的選擇,只是對於要求低延時的實時平臺Erlang是一個好的選擇

其餘

相關文章
相關標籤/搜索