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是一个好的选择

其余

相关文章
相关标签/搜索