Scala演员:接收与反应


110

首先让我说我有很多Java经验,但是直到最近才对函数式语言感兴趣。最近,我开始研究Scala,这似乎是一种非常不错的语言。

但是,我一直在Scala编程中阅读有关Scala的Actor框架的信息,有一件事我不理解。在30.4章中说,使用react代替receive可以重新使用线程,这对性能有好处,因为在JVM中线程很昂贵。

这是否意味着,只要我记得打电话react而不是receive,我就可以启动任意数量的Actor?在发现Scala之前,我一直在和Erlang一起玩,而Programming Erlang的作者自豪地介绍了20万个以上的过程而又不费吹灰之力。我讨厌用Java线程来做到这一点。与Erlang(和Java)相比,我在Scala中看到的是什么限制?

另外,该线程如何在Scala中重复使用?为了简单起见,我们假设我只有一个线程。我开始的所有参与者都将在该线程中按顺序运行,还是会进行某种任务切换?例如,如果我启动两个彼此进行乒乓球消息的演员,那么如果他们在同一线程中开始,是否会陷入僵局?

根据Scala编程的规定,编写要使用的演员比使用演员react困难receive。听起来似乎很合理,因为react没有回来。但是,本书继续说明了如何使用可以将react循环放入循环中Actor.loop。结果,您得到

loop {
    react {
        ...
    }
}

在我看来,这与

while (true) {
    receive {
        ...
    }
}

在本书的前面使用过。本书仍然指出,“实际上,程序至少需要几个receive”。那我在这里想念什么?除了回报之外还能receive做什么react?我为什么要在乎呢?

最后,进入我不了解的核心内容:这本书不断提到使用如何react使丢弃调用堆栈以重用线程成为可能。这是如何运作的?为什么有必要放弃调用堆栈?当函数通过抛出异常(react)终止而不能通过返回(receive)终止时,为什么不能丢弃调用堆栈呢?

我的印象是,Scala编程已经掩盖了这里的一些关键问题,这真是令人遗憾,因为否则它是一本真正优秀的书。


Answers:


78

首先,每个等待的演员receive都占用一个线程。如果它什么也没收到,那么该线程将永远不会做任何事情。行动者在react接收到任何东西之前不会占用任何线程。一旦它接收到某种东西,就会为其分配一个线程,并在其中对其进行初始化。

现在,初始化部分很重要。预期接收线程将返回某些内容,而反应线程则不会。因此,最后一个末尾的前一个堆栈状态react可以被完全丢弃。不需要保存或恢复堆栈状态,可以更快地启动线程。

出于各种性能原因,您可能想要一个或另一个。如您所知,在Java中拥有太多线程不是一个好主意。另一方面,由于必须先将actor附加到线程上react,所以receive消息的发送速度比消息快react。因此,如果您的演员接收到很多消息,但是却很少处理,那么额外的延迟react可能会使它对于您的目的来说太慢了。


21

答案是“是”-如果您的参与者没有阻塞代码中的任何内容,并且您正在使用react,那么您可以在单个线程中运行“并发”程序(尝试设置system属性actors.maxPoolSize以进行查找)。

之所以必须放弃调用堆栈,最明显的原因之一是,否则该loop方法将以结尾StackOverflowError。照原样,该框架react通过抛出a 来巧妙地结束了a SuspendActorException,它被循环代码捕获,然后该循环代码react通过andThen方法再次运行。

先看一下中的mkBody方法,再看一下Actorseq方法,看看循环如何重新安排自己的时间-非常聪明的东西!


20

这些“丢弃堆栈”的陈述也使我困惑了一段时间,我想我现在明白了,这就是我现在的理解。在“接收”的情况下,消息上有一个专用线程阻塞(在监视器上使用object.wait()),这意味着完整的线程堆栈可用,并准备从接收到的“等待”点继续执行信息。例如,如果您有以下代码

  def a = 10;
  while (! done)  {
     receive {
        case msg =>  println("MESSAGE RECEIVED: " + msg)
     }
     println("after receive and printing a " + a)
  }

该线程将在接收调用中等待,直到接收到该消息为止,然后继续执行并打印“接收并打印10之后”消息,并在线程阻塞之前的堆栈帧中显示“ 10”值。

在react没有这种专用线程的情况下,react方法的整个方法主体被捕获为一个闭包,并由接收消息的相应参与者上的任意线程执行。这意味着将只执行那些可以单独作为闭包捕获的语句,这就是“ Nothing”的返回类型起作用的地方。考虑以下代码

  def a = 10;
  while (! done)  {
     react {
        case msg =>  println("MESSAGE RECEIVED: " + msg)
     }
     println("after react and printing a " + a) 
  }

如果react的返回类型为void,则表示在“ react”调用之后使用语句是合法的(在示例中,println语句在“ react and print 10”之后打印消息),但实际上永远不会执行,因为仅捕获了“ react”方法的主体并对其进行排序以便以后(在消息到达时)执行。由于react合同的返回类型为“ Nothing”,因此react之后不能有任何语句,并且没有理由维护堆栈。在上面的示例中,由于根本不执行react调用之后的语句,因此不必维护变量“ a”。注意,react主体所需要的所有变量已经被捕获为闭包,因此可以很好地执行。

Java actor框架Kilim实际上是通过保存堆栈来进行堆栈维护的,该堆栈在响应消息时展开。


谢谢,这是非常有益的。但是,您不是+a在代码段中指的是,不是+10吗?
jqno

好答案。我也没明白。
santiagobasulto 2012年


0

我尚未在scala / akka上做任何重要工作,但是我知道演员的安排方式有很大的不同。Akka只是一个智能线程池,它按时间划分actor的执行时间...每个时间片都是一个actor执行直到完成的一条消息,这与Erlang不同(可能是每个指令)?

这使我认为反应更好,因为它暗示当前线程考虑其他参与者进行调度,因为接收“可能”会与当前线程接合以继续为同一参与者执行其他消息。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.