Actors in Scala(Scala中的Actor)(预打印版) 第五章 Event-Based Programming (A)

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 (事件和线程的对比)

基于事件的actor对程序员来讲并非是完全透明的。这是因为基于事件的编程与基于线程的编程遵循着不同的规范。典型的actor就是花费很久的时间等待事件到来(然后处理...),而基于事件的actor和基于线程的actor的关键区别就是:基于事件的actor具有等待的策略。

基于线程的actor通过对一个对象调用 wait()方法使得这个actor对应的线程持有锁并开始等待,当其他线程对相同的对象调用了 notify()或者 notifyAll()之后,持有锁的线程便恢复运行。(这是基本的原理,实际中的等待策略会稍微复杂些,因为线程在等待时可能被中断。)相反,基于事件的actor在actor运行时会注册一个事件处理器(event-handler)。注册后,actor的计算逻辑就完成了——之前运行这部分计算逻辑的线程响应的也就完成了任务,并且可以被调度出去运行其他任务了,如果没有其他事情可做的话该线程便进入睡眠状态。之后,当一个感兴趣的事件被触发时——即一个发送给该actor的并且被匹配上的事件到达时,actor的运行时调度器便调度该actor的事件处理器(event-handler)在线程池上执行,此时之前注册的事件处理器就会恢复运行,并处理该事件。在这种工作方式下,基于事件的actor就与底层JVM线程解除耦合了。

5.2 Making actors event-based: react (使得actor以基于事件的方式工作:react)

由于基于事件的actor和基于线程的actor在他们的等待策略上有很大的不同,把直截了当的把基于线程的actor转换成基于事件的actor。到目前为止我们看到的基于线程的actor都使用 receive方法来等待一条到达该actor的邮箱的并且符合匹配规则的消息,要把它转换成基于事件的actor,只要把所有调用 receive方法的地方都使用 react方法替换。receive和react方法都接收一堆消息匹配的语句块作为输入参数,这些语句块负责处理匹配上的消息。

虽然使用 react代替 receive仅仅是简单的代码改变,但是在程序中这两者的使用非常不同, 接下来的例子将揭示这些区别。

Using react to wait for messages(使用react等待消息)

下面的代码展示了一个方法构建了一个actor链,并且返回第一个actor。在actor链中的每个actor都使用react来等待一个叫做 'Die的消息。当它收到了这样一个消息,actor会检查它是否是此actor链中最后一个actor(如果是最后一个actor则next==null),如果不是最后一个actor,则给actor链中的下一个actor发送'Die消息,然后等待'Ack消息,当'Ack消息到达时,在自己终止之前给自己发送'Die消息的发送者发送'Ack响应,之后自己终止。如果是最后一个actor,则直接发送'Ack消息给发送者。注意,我们把最原始的发送'Die消息的发送者保存在本地变量from中,以至于可以在下面嵌套的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")
    }
}

当每个actor给链中的下一个actor发送'Die消息后等待'Ack时,将会发生什么呢?当'Ack消息收到后,它会向链中前一个actor传播'Ack,然后自己终止。链中第一个actor是最后一个收到'Ack消息的。当main方法中的receive操作收到'Ack消息开始处理时,链中的所有actor都终止了。

How many actors are too many?(多少actor才算多?)

使用react接收消息的actor比起通常JVM的线程相比轻量多了。下面我们就看看actor到底有多轻量级,我们将用尽所有的JVM内存创建一个actor链,然后我们通过替换把react替换成receive来和基于线程的actor链比较一下。

但是,首先能创建多少基于事件的actor?创建他们需要花费多长时间?在一个测试系统中,创建并销毁1000个actor花费了115ms,然而创建并销毁10000个actor花费了540ms。创建50万个actor花费了6323ms,但是创建100万个actor花费的时间稍微长些,在不增加JVM(Java HotSpot(TM) Server VM 1.6.0)的堆内存大小时,花费大概26秒。

下面我们尝试一下基于线程的actor。既然我们将要创建非常多的线程,我们应该配置actor的运行时环境以避免不合理的开销。

Configuring the actor run-time’s thread pool(配置actor的运行时线程池)

既然我们将用基于线程的actor创建很多线程,那么提前创建好这些线程然后再给actor使用将更高效。此外我们可以调整actor内部的运行时线程池来优化actor的执行。Scala的运行时环境允许根据在receive(每个receive块都需要自己的线程)中被阻塞的actor,动态改变线程池的大小,但是调整线程池大小会非常耗时,由于线程池没有为大量的调整线程池大小操作进行优化。

内部线程池通过两个JVM属性配置,分别是  actors.corePoolSizeactors.maxPoolSize。第一个属性是用来设置线程池初始化时的大小,第二个属性是用来限制线程池线程数量的上限。

为了最小化调整线程池线程数量所花费的时间,我们把这两个属性都设置成程序实际需要的线程数。比如,当用1000个基于线程的actor运行我们的actor链的例子时,把 actors.corePoolSize 设置为1000,把 actors.maxPoolSize 设置成1010,这样设置使得调整线程池大小的开销保持较低的状态。

设置了这些属性后,花费了12秒创建并销毁1000个基于线程的actor。创建并销毁2000个基于线程的actor花费了超过97秒。创建并销毁3000个actor,JVM抛出了java.lang.OutOfMemoryError。

就像这个简单的例子所证明的,基于事件的actor比基于线程的actor轻量多了。下面的章节如何使用有效的使用基于事件的actor编程。

Using react effectively(有效的使用react)

正像我们上面提到的,使用react等待消息的actor是以基于事件的方式工作。在这种工作方式下,actor等待消息时并不阻塞底层的工作线程,而是将reactor的模式匹配语句块注册成事件处理器。这个事件处理器会在actor的运行时环境中当匹配到的消息到达此actor时被调用。在actor进入睡眠状态前,事件处理器一直被保留着,这就是事件处理器的全部。特别的,当actor运行时,调用堆栈被当前的线程维护,当actor暂停时,调用堆栈就被丢弃。这种工作方式允许运行时系统释放底层的线程,以便此线程能够被其他actor重用。通过在比较小数量的线程上运行大量的基于事件的actor,CPU上下文切换以及与线程绑定的actor所需的资源消耗都显著降低了。

当基于事件的actor被暂停时,当前线程的调用堆栈被丢弃,这种工作模式在基于事件actor的编程模型中具有重要的后果:那就是调用react方法将不会正常返回。react,就像其他的Scala或者Java方法一样,当它执行时仅仅当它的全部调用堆栈可用时才可以正常的返回。但是基于事件的actor调用完毕后没有调用堆栈可用,因此调用react方法根本就不返回。

react方法不再返回,这意味着不再有任何代码紧跟在react方法之后。既然react不返回,跟在react方法之后的代码将不会执行。因此调用react方法必须总是基于事件actor在结束之前做的最后一件事。

既然actor的主要工作就是处理它所感兴趣的消息,并且react定义了基于事件的actor的消息处理机制,你可能认为react将总是最后一件事情、甚至仅仅是最后一件actor要做的事情。然而,有时候很方便的接连执行多个react调用。在这些情况下,你可以顺序嵌套react调用,就像之前的buildChain代码中展示的一样。

作为另一种选择,你可以定义一个递归方法依次返回多个react。比如,你可以扩展我们简单的链actor的例子,让actor等到固定数量的‘Die消息后终止。我们可以通过通过把链actor的处理消息的代码替换为一个waitFor方法,如下代码所示。waitFor方法预先测试,决定该actor是应该终止退出(if n == 0)还是继续等待消息。程序逻辑还和之前一样。区别仅仅是在把每个消息发送给from之后,我们添加了一个递归调用waitFor。

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的内部线程池等待执行。

执行这个任务的线程在调用堆栈上除了有线程池工作线程的基本逻辑之外没有太多的其他东西。其结果就是在调用堆栈上每一次调用react方法执行都如同空执行一样轻松。像waitFor这种递归方法的调用堆栈,因此不会因为反复调用react而增长太多。

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的线程资源。

使用上面的sleep方法,调用完react后,总要求执行一些东西。因为我们要重用方法,所以我们就不能简单的到代码前面在react代码体中插入一些逻辑。相反的,我们需要一种方式把sleep方法和在收到了 'Awake 消息之后执行的代码结合起来,而不改变sleep方法的实现。

我们所需要的功能正好是Actor对象的控制流组合器所提供的。这个组合器允许把公共的通讯模式用一种相对简单且简洁的方式表达出来。最基本的组合器就是 andThenandThen组合器将两块代码组合到一起,互相在对方之后运行,即使第一块代码调用了react。

下面的代码展示了如何在调用了sleep方法之后使用andThen执行代码。andThen的使用如同操作符一样,被嵌在两块代码之间。第一块代码的最后一个操作调用了sleep,在sleep内部最后又调用了react。
actor {
  val period = 1000
  {
    //sleep之前的代码
    sleep(period)
    andThen {
      //唤醒之后的代码
    }
  }
}

注意,sleep函数的参数period在andThen代码块之外声明。这样做是可以的,因为这两块代码块都是闭包,闭包能够在他们的运行上下文中捕获变量。第二块代码会在第一块代码结束后运行,即便第一块代码的sleep方法内有react方法调用。然而,注意第二块代码是被actor执行的最后一块代码。andThen的使用并没有改变react方法不会返回的事实,andThen的作用仅仅是将两块代码顺序组合起来而已。

另外一个有用的组合器就是loopWhile。就像它的名字建议的那样,如果提供的条件为真的话,那么输入的闭包代码将会持续循环执行。多亏了Scala灵活的语法,loopWhile感觉就像语言原生的语法一样。下面的代码展示了一个变动的actor链的例子,这个例子使用loopWhile等待多个 'Die 消息。loopWhile有两个代码块,分别是条件(n > 0)和循环体,这两者都是闭包,因为这两者都访问了本地变量n。注意,循环体中最顶层的react自从最开始的例子以来一直都没有改变过。循环体也可以被抽象成一个方法,loopWhile在这两种情况下都能正常工作。

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
}


  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值