Actors in Scala(Scala中的Actor)(预打印版) 第五章 Event-Based Programming (A)
张贵宾
guibin.beijing@gmail.com
2011.10.27
注:翻译这些英文书籍资料纯属个人爱好,如有不恰当之处敬请指正。
我们第二章介绍的概念都是把每个actor与JVM线程联系起来了:每个actor都需要自己专用的线程。如果你的程序需要比较少的actor,那么每个actor对应一个线程的工作方式没有什么问题。
如果你期待更多的actor,或者程序中actor的数量随着输入的增加而增加,那么定义每个actor对应一个线程的工作方式将会带来巨大的开销:不仅每个JVM线程的执行堆栈需要内存——这部分堆栈内存通常是预分配的——每条JVM线程都还与底层操作系统的进程对应。对于不同的平台,进程间上下文的切换(CPU对进程的切换),cpu在内核模式和用户模式切换,这些都是昂贵的开销。
为了允许在JVM中有许多actor,你可以使得你的actor是基于事件的。基于事件的actor可以被实现成事件处理器(event handlers),并非线程,因此更轻量级,而不象线程的兄弟般一样重量级。既然基于消息的actor将不直接绑定到Java线程上,那么基于事件的actor就可以在一个拥有较少数量工作线程的线程池上工作。典型的,这样一个线程池应该包含和系统处理器数量一样多的工作线程。这样做可以最大化系统的并行性能,使得线程池中线程占用的内存数、系统进程间上下文切换这些开销达到最小。
5.1 Events vs. threads (事件和线程的对比)
5.2 Making actors event-based: react (使得actor以基于事件的方式工作:react)
Using react to wait for messages(使用react等待消息)
def buildChain(size: Int, next: Actor): Actor = {
val a = actor {
react {
case 'Die =>
val from = sender
if(next != null) {
next ! 'Die
react {
case 'Ack => from ! 'Ack
}
} else from ! 'Ack
}
}
if(size > 0) buildChain(size - 1, a)
else a
}
我们把buildChain方法放进一个具有main函数的对象中,如下面的代码所示。我们把命令行中的第一个参数存储在numActors变量中,这个变量用来控制actor链的长度,仅仅为了好玩,我们标记了时间来看它花费多久来建立并且销毁一个单元素的actor链。在调用了buildChain之后,我们立即给链中的第一个actor发送了一条'Die消息。
def main(args: Array[String]) {
val numActors = args(0).toInt
val start = System.currentTimeInMillis
buildChain(numActors, null) ! 'Die
receive {
case 'Ack =>
val end = System.currentTimeInMillis
println("Took " + (end - start) + " ms")
}
}
How many actors are too many?(多少actor才算多?)
使用react接收消息的actor比起通常JVM的线程相比轻量多了。下面我们就看看actor到底有多轻量级,我们将用尽所有的JVM内存创建一个actor链,然后我们通过替换把react替换成receive来和基于线程的actor链比较一下。Configuring the actor run-time’s thread pool(配置actor的运行时线程池)
Using react effectively(有效的使用react)
正像我们上面提到的,使用react等待消息的actor是以基于事件的方式工作。在这种工作方式下,actor等待消息时并不阻塞底层的工作线程,而是将reactor的模式匹配语句块注册成事件处理器。这个事件处理器会在actor的运行时环境中当匹配到的消息到达此actor时被调用。在actor进入睡眠状态前,事件处理器一直被保留着,这就是事件处理器的全部。特别的,当actor运行时,调用堆栈被当前的线程维护,当actor暂停时,调用堆栈就被丢弃。这种工作方式允许运行时系统释放底层的线程,以便此线程能够被其他actor重用。通过在比较小数量的线程上运行大量的基于事件的actor,CPU上下文切换以及与线程绑定的actor所需的资源消耗都显著降低了。def waitFor(n: Int): Unit = if(n > 0) {
react {
case 'Die =>
val from = sender
if(next != null) {
next ! 'Die
react {
case 'Ack => from ! 'Ack; waitFor(n - 1)
}
} else {from ! 'Ack; waitFor(n - 1)}
}
}
Recursive methods with react(使用react的递归方法)
看到上面的代码,你可能关注以这种方式递用递归方法可能会很快导致堆栈溢出,不过好消息是react方法与递归配合的非常好。无论任何时候恢复调用react方法时,都是由于actor的邮箱中收到了匹配的消息,此时会创建一个计算任务并提交到actor的内部线程池等待执行。Composing react-based code with combinators(使用组合器把基于react的代码组合起来)
有时候很难或者不可能为定序的多个react使用递归方法,当使用react重用类或者方法时就会遇到这种情况。从重用的本质上讲,被重用的组件在构建之后应该不能再改动,尤其是我们不能进行侵略性的改变,比如我们用递归的方式为上面的例子代码添加一个迭代方法。本节将讲解几种基于react代码的重用方式。def sleep(delay: Long) {
register(time, delay, self)
react {
case 'Awake => //OK, Continue
}
}
比如,假设我们的项目中包含如上所示的sleep方法。此方法使用了定时器服务(代码中未列出来)注册了当前的actor:self,定时器会在指定的延时delay之后被唤醒。定时器会用 'Awake 消息通知注册的actor。为了提高效率,sleep方法用react等待 'Awake 消息,这样做可以使得处于睡眠状态的actor不需要消耗JVM的线程资源。
actor {
val period = 1000
{
//sleep之前的代码
sleep(period)
andThen {
//唤醒之后的代码
}
}
}
注意,sleep函数的参数period在andThen代码块之外声明。这样做是可以的,因为这两块代码块都是闭包,闭包能够在他们的运行上下文中捕获变量。第二块代码会在第一块代码结束后运行,即便第一块代码的sleep方法内有react方法调用。然而,注意第二块代码是被actor执行的最后一块代码。andThen的使用并没有改变react方法不会返回的事实,andThen的作用仅仅是将两块代码顺序组合起来而已。
def buildChain(size: Int, next: Actor, waitNum: Int): Actor = {
val a = actor {
var n = waitNum
loopWhile (n > 0) {
n -= 1
react {
case 'Die =>
val from = sender
if (next != null) {
next ! 'Die
react {case 'Ack => from ! 'Ack}
} else from ! 'Ack
}
}
}
if (size > 0) buildChain(size - 1, a, waitNum)
else a
}