Erlang是一门被设计用来开发并发编程、软实时、分布式、具有容错系统的语言。
基于原生的并发支持,Erlang提供了一种在多核系统的高效软件开发方式,程序开发者可以通过生成轻量级Erlang进程的方式同时执行任务。
实时系统的调度器把这些进程的工作负载自动分发到不同核心上。所以Erlang VM调度器的特性直接关系着Erlang在多核平台的性能。
什么是调度?
通常来说,调度就是一种分派工作给工作者的机制。调度器通过调度算法执行调度活动,最大限度地提高吞吐量和公平性,降低响应时间和延时。调度是多任务系统的重要组成部分,它被分为两种类型:
抢占式:一个抢占式调度器在执行的任务间进行上下文切换,它有权力抢占(中断)任务并且在不需要被抢占任务的配合下的稍后恢复执行它们。实现这样的功能是基于如下几个因素,比如:任务的优先级,时间切片或者规约数。
协作式:一个协作式调度器需要任务协作来进行上下文切换。在这种方式下,调度器简单地让任务周期性地或者空闲地时候自愿地释放控制权,然后启动一个新的任务并且再一次等待它自愿地归还控制权。
现在的问题是,哪一种调度机制适合软实时系统,也就是这个系统必须在指定的时间内响应。协作式调度系统不能满足软实时系统的要求,因为其运行的任务可能永远也不会返还控制权或者在规定时限后返还控制权。所以软实时系统通常采用抢占式调度。
Erlang的调度是什么样子的?
对于Erlang来说有四种类型的工作需要调度:进程、端口、链接驱动程序、系统级活动。
进程和端口不必多说。
系统级任务包括检查I/O活动,比如Erlang终端的用户输入。
链接驱动程序是另一种将用其他语言编写的外部程序集成到Erlang中的机制。外部程序在独立的操作系统进程执行,通过普通的端口,外部程序作为一个连接驱动程序,在Erlang节点的操作系统进程的一个线程中执行。它依赖于端口去和其他Erlang进程通信。
总览
Erlang采用抢占式调度,调度器的职责就是选择一个进程并执行,同时负责处理垃圾回收和内存管理。
R11B之前的调度
R11B之前,Erlang还不支持SMP,只有一个调度器和一个运行队列。调度器从运行队列中选择可以执行的Erlang进程和IO任务执行,单线程访问运行队列,不需要加锁。
R11B和R12B的调度
R11B引入了SMP后,Erlang VM可以有1~1024个调度器,每个调度器运行在一个线程内。所有的调度器从一个公共运行队列中获取Erlang进程和IO任务执行。
因为存在共享数据结构,所以运行队列需要锁进行保护,损失了一定的性能。使用锁保护共享数据结构,使得只有一个调度器的SMP VM比非SMP VM慢10%左右。如果长时间没有锁争用的话,开销也没有很大。
但是仍然有一些已知的瓶颈:
- 公共运行队列
当CPU或者核心数增多时,单公共运行队列就是一个明显的瓶颈。 - Ets表
Ets表引入了锁,如果多个Erlang进程访问同一个表,就会造成大量锁争用,影响性能,同时也会影响到Mensia。Ets表的锁是表锁,不是record锁。一个解决方案就是引入更细粒度的锁。 - 消息传递
当多个进程发消息时,一些接收进程就会产生大量锁争用。通过减少锁的数量同时又能完成任务可以进行优化。 - 进程可以阻塞调度器
如果某个进程在等待获取锁(例如访问ets-table)的过程中被阻塞,则整个调度程序都会被阻塞,直到获得正确的锁并且该进程可以继续执行为止。
R13B之后的调度
这个版本之后,每个调度器都有自己的运行队列,可以减少锁争用的数量并提升整体性能。
Reduction Counting方法
Erlang调度器基于reduction counting的方法来度量执行时间的。
一个reduction粗略的等于函数调用的时间。因为每个函数调用占用不同长度的时间,不同reduction的实际期间并不同。当调度进程去执行时,会分配给它一定数量的reduction用来去执行(R13B04版本默认分配2000 reduction)。进程将一直执行,直到消耗完所有reduction或者等待一条消息。如果新消息到达或者超时,等待消息的进程将被重新调度。重新调度或者新进程将被放到当前运行队列的末尾。挂起(阻塞)的进程不会储存在运行队列中。
进程优先级
有四种进程优先级:maximum,high,normal,low。
每个进程的maximun和high优先级都各自有一个队列,normal和low优先级及共享一个队列。因此在调度器的运行队列中,每个进程有3个队列。端口也有一个队列。每个进程优先级的队列或端口的队列,在后文中都被成为优先级队列。总的来说,一个调度器运行队列包含四个优先级队列,保存所有可运行的进程和端口。运行队列的所有优先级队列里的进程和端口数被当做运行队列长度。
同一优先级队列的进程通过轮询执行。轮询是一种将等量时间片(这里是reduction数)按循环顺序分配给每个进程的算法,每个进程有相同的执行优先级。
调度器选择maximum队列进程去执行,直到队列为空。然后对high优先级队列做同样的处理。当maximum和high优先级队列中没有进程时,才会去执行normal优先级进程。因为low优先级和normal优先级进程在同一个队列,通过在执行前多次跳过low优先级进程来区分优先级。
调度器数量
启动Erlang VM的时候可以配置调度器线程的数量。
默认情况下调度器数量等于系统逻辑处理器个数。一个核心或者一个硬件线程是一个逻辑处理器。默认全部调度器都是可用的,在启动Erlang VM的时候用户也可以值让一部分调度器线程在线或者保持可用状态。