110

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

但是,我在Programming in Scala中一直在阅读有关 Scala 的 Actor 框架的内容,但有一件事我不明白。在第 30.4 章中,它说使用react而不是receive可以重用线程,这对性能有好处,因为线程在 JVM 中很昂贵。

这是否意味着,只要我记得 callreact而不是receive,我就可以启动任意数量的 Actors 吗?在发现 Scala 之前,我一直在使用 Erlang,而Programming Erlang的作者吹嘘自己毫不费力地生成了超过 200,000 个进程。我讨厌用 Java 线程来做这件事。与 Erlang(和 Java)相比,我在 Scala 中看到了什么样的限制?

另外,这个线程如何在 Scala 中重用?为简单起见,我们假设我只有一个线程。我开始的所有演员会在这个线程中按顺序运行,还是会发生某种任务切换?例如,如果我启动两个相互发送 ping-pong 消息的演员,如果他们在同一个线程中启动,我会面临死锁的风险吗?

根据Programming in Scala,编写actors 来使用react比with 更难receive。这听起来很合理,因为react不会返回。然而,这本书继续展示了如何react使用Actor.loop. 结果,你得到

loop {
    react {
        ...
    }
}

对我来说,这似乎很相似

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

本书前面使用过。尽管如此,这本书还是说“在实践中,程序至少需要几个receive's”。那么我在这里错过了什么?除了回报,还有什么不能receive做的?react我为什么要关心?

最后,来到我不明白的核心:这本书一直提到 using 如何react使丢弃调用堆栈以重用线程成为可能。这是如何运作的?为什么需要丢弃调用堆栈?为什么当函数通过抛出异常 () 终止时可以丢弃调用堆栈react,而当它通过返回 ( receive) 终止时不能?

我的印象是,Scala 中的编程一直在掩盖这里的一些关键问题,这是一种耻辱,因为否则它是一本真正优秀的书。

4

5 回答 5

78

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

现在,初始化部分很重要。接收线程应该会返回一些东西,而反应线程则不会。因此,最后一个堆栈状态结束时的先前堆栈状态react可以并且是完全丢弃。不需要保存或恢复堆栈状态使线程更快启动。

您可能需要一个或另一个的性能原因有多种。如您所知,在 Java 中拥有太多线程并不是一个好主意。另一方面,因为您必须先将参与者附加到线程,所以它比消息react更快。因此,如果您的参与者接收到许多消息但对其执行的操作很少,那么额外的延迟可能会使其速度太慢而无法达到您的目的。receivereactreact

于 2009-08-09T16:43:31.497 回答
21

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

需要丢弃调用堆栈的一个更明显的原因是,否则该loop方法将以StackOverflowError. 事实上,框架相当巧妙地react通过抛出 a 来结束 a SuspendActorException,它被循环代码捕获,然后react通过该andThen方法再次运行。

看看mkBody方法Actor,然后seq看看循环如何重新安排自己的方法 - 非常聪明的东西!

于 2009-08-11T06:25:06.227 回答
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 并打印 10”之后),但实际上永远不会被执行,因为只有“react”方法的主体被捕获并排序以便稍后执行(在消息到达时)。由于 react 合约的返回类型为“Nothing”,因此 react 后面不能有任何语句,因此没有理由维护堆栈。在上面的示例中,不必维护变量“a”,因为根本不执行反应调用之后的语句。请注意,react 主体所需的所有变量都已被捕获为闭包,因此它可以正常执行。

Java Actor 框架Kilim实际上通过保存在收到消息的反应时展开的堆栈来进行堆栈维护。

于 2010-06-07T03:15:49.117 回答
8

只是在这里:

没有控制反转的基于事件的编程

这些论文链接自 Actor 的 scala api,并为 Actor 实现提供了理论框架。这包括为什么 react 可能永远不会返回。

于 2010-06-15T14:47:55.503 回答
0

我没有用 scala /akka 做过任何重要的工作,但是我知道演员的安排方式有很大的不同。Akka 只是一个智能线程池,它可以对 Actor 的执行进行时间切片......每个时间切片都将是一个消息执行到由一个 Actor 完成,这与 Erlang 中的可能是每条指令不同?!

这让我认为 react 更好,因为它提示当前线程考虑其他参与者进行调度,因为接收“可能”让当前线程继续为同一参与者执行其他消息。

于 2014-03-11T18:53:27.790 回答