入门指南 (Scala)

原文:http://www.gtan.com/akka_doc/intro/getting-started-first-scala.html

序言

欢迎来到第一个使用Akka和Scala的指南。我们假设你已经知道Akka和Scala是什么,现在需要了解开始第一个项目的步骤。

本指南有两种方式:

  • 创建一个独立项目,从命令行运行
  • 创建SBT项目,在SBT中运行

因为这两种方式非常相像,我们都会进行讲解。

我们要创建的示例应用是使用actor来计算PI的值。计算PI是一项CPU密集的操作,我们将使用Akka Actor来编写一个可以垂直扩展到多个处理器核上的并发解决方案。在将来的指南中,这个示例应用将被扩展国使用Akka远程Actor来在水平到集群中的多台机器上。

我们所使用的算法叫“embarrassingly parallel” 意思是每个子任务是独立完成的,与其它子任务无关。这个算法可以高度并行化,所以非常适合使用actor模型。

以下是我们所使用的算法的公式:

在这个特定的算法中,有一个主actor将序列分割成段并发送给工作actor来进行计算。当工作actor完成自己的序列段的计算后将结果传给主actor,由主actor进行汇总。

源码

如果你不想把源码用键盘敲一遍而且/或不想创建SBT项目,你可以从Akka GitHub仓库中下载整个指南。它的位置在akka-tutorials/akka-tutorial-first模块. 你也可以到这里在线浏览。实际的代码在这里。

要使用Git下载代码运行下面的命令:

Linux/Unix/Mac 系统:

 
      
  1. $ git clone git://github.com/akka/akka.git
  2. $ cd akka/akka-tutorials/akka-tutorial-first

Windows 系统:

 
      
  1. C:\Users\jboner\src> git clone git://github.com/akka/akka.git
  2. C:\Users\jboner\src> cd akka\akka-tutorials\akka-tutorial-first

准备工作

本指南假设你安装了Java 1.6或更高版本并且java命令在你的PATH上. 你还需要知道如何在shell(ZSH, Bash, DOS 等等.)中运行命令,需要一个文本编辑器或IDE来输入Scala代码。

你必须保证$JAVA_HOME环境变量被正确设置为Java安装位置的根目录,还必须保证$JAVA_HOME/bin在你的PATH上。

Linux/Unix/Mac 系统:

 
     
  1. $ export JAVA_HOME=..root of Java distribution..
  2. $ export PATH=$PATH:$JAVA_HOME/bin

检测java正确安装:

 
     
  1. $ java -version
  2. java version "1.6.0_24"
  3. Java(TM) SE Runtime Environment (build 1.6.0_24-b07-334-10M3326)
  4. Java HotSpot(TM) 64-Bit Server VM (build 19.1-b02-334, mixed mode)

Windows 系统:

 
     
  1. C:\Users\jboner\src\akka> set JAVA_HOME=..root of Java distribution..
  2. C:\Users\jboner\src\akka> set PATH=%PATH%;%JAVA_HOME%/bin

检测java正确安装:

 
     
  1. C:\Users\jboner\src\akka> java -version
  2. java version "1.6.0_24"
  3. Java(TM) SE Runtime Environment (build 1.6.0_24-b07-334-10M3326)
  4. Java HotSpot(TM) 64-Bit Server VM (build 19.1-b02-334, mixed mode)

下载安装 Akka

要从命令行编译和运行本指南示例,需要下载Akka。如果你希望用SBT来编译和运行它,建议跳过这一部分直接看下一节。

从http://akka.io/downloads/下载akka-2.0.zip发布包,它包含本指南所需的全部模块。下载完成后,将发布包解压至安装目录。我的安装目录下/Users/jboner/tools/

要正确安装Akka,需要再做一件事:设置AKKA_HOME环境变量到安装目录的根.

Linux/Unix/Mac 系统:

 
     
  1. $ cd /Users/jboner/tools/akka-2.0
  2. $ export AKKA_HOME=`pwd`
  3. $ echo $AKKA_HOME
  4. /Users/jboner/tools/akka-2.0

Windows 系统:

 
     
  1. C:\Users\jboner\src\akka> cd akka-2.0
  2. C:\Users\jboner\src\akka\akka-2.0> set AKKA_HOME=%cd%
  3. C:\Users\jboner\src\akka\akka-2.0> echo %AKKA_HOME%
  4. C:\Users\jboner\src\akka\akka-2.0

安装路径的内容如下:

Linux/Unix/Mac 系统:

 
     
  1. $ ls -1
  2. bin
  3. config
  4. deploy
  5. doc
  6. lib
  7. src

Windows 系统:

 
     
  1. C:\Users\jboner\src\akka\akka-2.0> dir
  2. bin
  3. config
  4. deploy
  5. doc
  6. lib
  7. src
  • bin目录中是用来启动Akka微内核的脚本。
  • In theconfig目录中是Akka配置文件。
  • In thedeploy目录用来放置随微内核一起运行的应用。
  • In thedoc目录中是文档、API的jar包。
  • In thelib目录中是Scala和Akka的jar包。
  • In thesrc目录中是Akka源码jar包。

本指南所需要的唯一一个jar包 (除了scala-library.jar以外) 是lib/akka目录中的akka-actor-2.0.jar. 这个jar包没有外部依赖,有了它我们就可以编写一个使用actor的系统。

Akka模块化做得很好,包含有实现不同功能的各个jar包,其模块包括:

  • akka-actor– Actor
  • akka-remote– 远程 Actor
  • akka-slf4j– SLF4J 事件处理监听器
  • akka-testkit– 测试actor的工具包
  • akka-kernel– 运行一个基础的最小应用服务器的微内核
  • akka-durable-mailboxes– 持久邮箱: 基于文件, MongoDB, Redis, Zookeeper
  • akka-amqp– AMQP 集成

下载安装 Scala

要从命令行编译和运行本指南示例,需要安装Scala发布包。如果你要使用SBT,在SBT中编译和运行,可以跳过这一部分直接进入下一节。

可以从http://www.scala-lang.org/downloads下载Scala. 在那里下载 Scala 2.9.1 版本. 如果你选择的是tgzzip包则需要进行解压。如果选择的是IzPack安装程序,只需要双击它然后按照指示操作。

还必须保证scala-2.9.1/bin(scala-2.9.1是你的Scala安装目录) 在你的PATH上。

Linux/Unix/Mac 系统:

 
    
  1. $ export PATH=$PATH:scala-2.9.1/bin

Windows 系统:

 
     
  1. C:\Users\jboner\src\akka\akka-2.0> set PATH=%PATH%;scala-2.9.1\bin

你可以运行scala命令来测试你的安装是否正确。

Linux/Unix/Mac 系统:

 
     
  1. $ scala -version
  2. Scala code runner version 2.9.1.final -- Copyright 2002-2011, LAMP/EPFL

Windows 系统:

 
     
  1. C:\Users\jboner\src\akka\akka-2.0> scala -version
  2. Scala code runner version 2.9.1.final -- Copyright 2002-2011, LAMP/EPFL

看来一切顺利. 最后让我们创建一个源码文件Pi.scala并将它放在Akka发布版本的根目录下的tutorial目录 (如果这个目录不存在你需要创建它)。

有一些工具要求你设置SCALA_HOME环境变量为Scala发布包的根目录,不过Akka并没有这一要求。

下载安装SBT

SBT, Simple Build Tool的简称, 是用Scala语言编写的优秀的build工具。 它使用Scala语言来编写build脚本,功能非常强大。它拥有一个插件体系,已经有很多插件可供使用,我们很快就会用到它们。SBT是用来build用Scala编写的软件的推荐方法,而且可能是学习本指南的最简单的方法。如果你决定使用SBT,请按下面的指示操作,否则可以跳过这一部分和下一部分内容。

要安装SBT并创建本指南的项目,最简单的方法见https://github.com/harrah/xsbt/wiki/Setup。

现在我们需要创建我们的第一个Akka项目,你可以手动向build脚本中添加依赖,不过更简单的方法是使用下一部分中介绍的Akka SBT插件。

创建Akka SBT项目

如果你还没做过,那么现在就开始创建本指南所讲的SBT项目,所要做的是在你希望创建项目的目录下添加一个build.sbt文件:

 
     
  1. name := "My Project"
  2. version := "1.0"
  3. scalaVersion := "2.9.1"
  4. resolvers += "Typesafe Repository" at "http://repo.typesafe.com/typesafe/releases/"
  5. libraryDependencies += "com.typesafe.akka" % "akka-actor" % "2.0"

再创建一个名为src/main/scala的目录来存放源码.

虽然本指南并不需要,但是你可能愿意添加除了akka-actor外的其它Akka模块, 这些是添加在build.sbtlibraryDependencies部分。 注意其中的每一项之间必须有一个空行。下面是一个添加akka-remote的例子:

 
     
  1. libraryDependencies += "com.typesafe.akka" % "akka-actor" % "2.0"
  2. libraryDependencies += "com.typesafe.akka" % "akka-remote" % "2.0"

好,现在我们都弄好了。

SBT 自身有一堆依赖,不过我们的项目只需要其中一个:akka-actor-2.0.jar. SBT 会下载它。

开始编写代码

终于可以开始写代码了。

我们先创建Pi.scala文件并在文件顶部添加以下这些 import:

 
     
  1. import akka.actor._
  2. import akka.routing.RoundRobinRouter
  3. import akka.util.Duration
  4. import akka.util.duration._

如果你使用SBT,那么将 Pi.scala 放在src/main/scala目录下.

如果你使用命令行,那么可以将它放在随便哪儿。我是在Akka安装目录下创建了一个名为tutorial的目录, 也就是$AKKA_HOME/tutorial/Pi.scala

创建消息

我们要做的设计是由一个actor来启动整个计算过程,创建一组工作actor. 整个工作会被分割成具体的小段, 各小段会以round-robin的方式发送到不同的工作 actor. 主actor等待所有的工作actor完全各自的工作并将其回送的结果进行汇总。当计算完成以后,主actor将结果发送给监听器acotr, 由它来输出结果。

在这个基础上, 现在让我们创建在这个系统中流动的消息。我们需要4种不同的消息:

  • Calculate– 发送给actor 来启动计算。
  • Work– 从actor 发送给各工作actor,包含工作分配的内容。
  • Result– 从工作actors 发送给actor,包含工作actor的计算结果。
  • PiApproximation– 从actor发送给监听器actor,包含pi的最终计算结果和整个计算耗费的时间。

发送给actor的消息应该永远是不可变的,以避免共享可变状态。 在scala里我们有 ‘case classes’ 来构造完美的消息。现在让我们用case class创建3种消息。我们还为消息们创建一个通用的基础trait(定义为sealed以防止在我们不可控的地方创建消息):

 
     
  1. sealed trait PiMessage
  2. case object Calculate extends PiMessage
  3. case class Work(start: Int, nrOfElements: Int) extends PiMessage
  4. case class Result(value: Double) extends PiMessage
  5. case class PiApproximation(pi: Double, duration: Duration)

创建工作 actor

现在我们来创建工作 actor。 方法是混入Actortrait 并定义其中的receive方法.receive方法定义我们的消息处理器。我们让它能够处理Work消息,所以添加一个针对这种消息的处理器:

 
     
  1. class Worker extends Actor {
  2. // calculatePiFor ...
  3. def receive = {
  4. case Work(start, nrOfElements)
  5. sender ! Result(calculatePiFor(start, nrOfElements)) // perform the work
  6. }
  7. }

可以看到我们现在创建了一个Actor和一个receive方法作为Work消息的处理器. 在这个处理器中我们调用calculatePiFor(..)方法, 将结果包在Result消息里并使用sender异步发送回消息的原始发送者。 在Akka里,sender引用是与消息一起隐式发送的,这样接收者可以随时回复或将sender引用保存起来以备将来使用。

现在在我们的Workeractor 中唯一缺少的就是实现calculatePiFor(..)方法。 虽然在Scala里我们可以有很多方法来实现这个算法,在这个入门指南中我们选择了一种命令式的风格,使用了for写法和一个累加器:

 
     
  1. def calculatePiFor(start: Int, nrOfElements: Int): Double = {
  2. var acc = 0.0
  3. for (i start until (start + nrOfElements))
  4. acc += 4.0 * (1 - (i % 2) * 2) / (2 * i + 1)
  5. acc
  6. }

创建主actor

主actor会稍微复杂一些。 在它的构造方法里我们创建一个round-robin的路由器来简化将工作平均地分配给工作actor们的过程,先做这个:

 
     
  1. val workerRouter = context.actorOf(
  2. Props[Worker].withRouter(RoundRobinRouter(nrOfWorkers)), name = "workerRouter")

现在我们有了一个路由,可以在一个单一的抽象中表达所有的工作actor。现在让我们创建主actor. 传递给它三个整数变量:

  • nrOfWorkers– 定义我们会启动多少工作actor
  • nrOfMessages– 定义会有多少整数段发送给工作actor
  • nrOfElements– 定义发送给工作actor的每个整数段的大小

下面是主actor:

 
     
  1. class Master(nrOfWorkers: Int, nrOfMessages: Int, nrOfElements: Int, listener: ActorRef)
  2. extends Actor {
  3. var pi: Double = _
  4. var nrOfResults: Int = _
  5. val start: Long = System.currentTimeMillis
  6. val workerRouter = context.actorOf(
  7. Props[Worker].withRouter(RoundRobinRouter(nrOfWorkers)), name = "workerRouter")
  8. def receive = {
  9. // handle messages ...
  10. }
  11. }

有一些需要进一步解释的事。

注意我们向actor传进了一个ActorRef. 这是用来向外界报告最终的计算结果。

但是还没完。我们还缺少actor的消息处理器. 这个处理器需要能够对两种消息进行响应:

  • Calculate– 用来启动计算过程
  • Result– 用来汇总不同的计算结果

Calculate处理器会通过其路由器向所有的工作actor 发送工作内容.

Result处理器从Result消息中获取值并汇总到我们的pi成员变量中. 我们还会记录已经接收的结果数据的数量,它是否与发送出去的任务数量一致 。actor 发现计算完成了,会将最终结果发送给监听者. 当整个过程都完成了,它会调用context.stop(self)方法来终止自己它所监管的所有actor. 在本例中,主actor监管一个actor,我们的路由器,而路由器监管着所有nrOfWorkers个工作actors. 所有的actor都会在其监管者的stop方法被调用时自动终止,并会传递给所有它监管的子actor。

让我们在代码中实现这些:

 
     
  1. def receive = {
  2. case Calculate
  3. for (i 0 until nrOfMessages) workerRouter ! Work(i * nrOfElements, nrOfElements)
  4. case Result(value)
  5. pi += value
  6. nrOfResults += 1
  7. if (nrOfResults == nrOfMessages) {
  8. // Send the result to the listener
  9. listener ! PiApproximation(pi, duration = (System.currentTimeMillis - start).millis)
  10. // Stops this actor and all its supervised children
  11. context.stop(self)
  12. }
  13. }

创建计算结果监听者

监听者很简单,当它接收到从Master发来的PiApproximation,就将结果打印出来并关闭整个Actor系统

 
     
  1. class Listener extends Actor {
  2. def receive = {
  3. case PiApproximation(pi, duration)
  4. println("\n\tPi approximation: \t\t%s\n\tCalculation time: \t%s"
  5. .format(pi, duration))
  6. context.system.shutdown()
  7. }
  8. }

启动计算

现在只剩下实现启动和运行计算的执行者了。我们创建一个调用Pi的对象, 这里我们可以继承Scala中的Apptrait, 这个trait使我们能够在命令行上直接运行这个应用.

Pi对象是我们的actor和消息的很好的容器。所以我们把它们都放在这儿。我们还创建一个calculate方法来启动actor 并等待它结束:

 
     
  1. object Pi extends App {
  2. calculate(nrOfWorkers = 4, nrOfElements = 10000, nrOfMessages = 10000)
  3. // actors and messages ...
  4. def calculate(nrOfWorkers: Int, nrOfElements: Int, nrOfMessages: Int) {
  5. // Create an Akka system
  6. val system = ActorSystem("PiSystem")
  7. // create the result listener, which will print the result and shutdown the system
  8. val listener = system.actorOf(Props[Listener], name = "listener")
  9. // create the master
  10. val master = system.actorOf(Props(new Master(
  11. nrOfWorkers, nrOfMessages, nrOfElements, listener)),
  12. name = "master")
  13. // start the calculation
  14. master ! Calculate
  15. }
  16. }

以上的calculate方法创建一个Actor系统,这是包括所有创建出的actor的 “上下文”。 如何在容器中创建actor的例子在calculate方法的‘system.actorOf(...)’这一行。 这里我们创建两个顶级actor. 如果你是在一个actor上下文(i.e. 在一个创建其它actor的actor中),你应该使用context.actorOf(...). 这在以上的主actor代码中有所体现。

好了,终于完成了。

但是在打包和运行之前,让我们看看完整的代码,包括package定义,import:

 
     
  1. /**
  2. * Copyright (C) 2009-2012 Typesafe Inc.
  3. */
  4. package akka.tutorial.first.scala
  5. import akka.actor._
  6. import akka.routing.RoundRobinRouter
  7. import akka.util.Duration
  8. import akka.util.duration._
  9. object Pi extends App {
  10. calculate(nrOfWorkers = 4, nrOfElements = 10000, nrOfMessages = 10000)
  11. sealed trait PiMessage
  12. case object Calculate extends PiMessage
  13. case class Work(start: Int, nrOfElements: Int) extends PiMessage
  14. case class Result(value: Double) extends PiMessage
  15. case class PiApproximation(pi: Double, duration: Duration)
  16. class Worker extends Actor {
  17. def calculatePiFor(start: Int, nrOfElements: Int): Double = {
  18. var acc = 0.0
  19. for (i start until (start + nrOfElements))
  20. acc += 4.0 * (1 - (i % 2) * 2) / (2 * i + 1)
  21. acc
  22. }
  23. def receive = {
  24. case Work(start, nrOfElements)
  25. sender ! Result(calculatePiFor(start, nrOfElements)) // perform the work
  26. }
  27. }
  28. class Master(nrOfWorkers: Int, nrOfMessages: Int, nrOfElements: Int, listener: ActorRef)
  29. extends Actor {
  30. var pi: Double = _
  31. var nrOfResults: Int = _
  32. val start: Long = System.currentTimeMillis
  33. val workerRouter = context.actorOf(
  34. Props[Worker].withRouter(RoundRobinRouter(nrOfWorkers)), name = "workerRouter")
  35. def receive = {
  36. case Calculate
  37. for (i 0 until nrOfMessages) workerRouter ! Work(i * nrOfElements, nrOfElements)
  38. case Result(value)
  39. pi += value
  40. nrOfResults += 1
  41. if (nrOfResults == nrOfMessages) {
  42. // Send the result to the listener
  43. listener ! PiApproximation(pi, duration = (System.currentTimeMillis - start).millis)
  44. // Stops this actor and all its supervised children
  45. context.stop(self)
  46. }
  47. }
  48. }
  49. class Listener extends Actor {
  50. def receive = {
  51. case PiApproximation(pi, duration)
  52. println("\n\tPi approximation: \t\t%s\n\tCalculation time: \t%s"
  53. .format(pi, duration))
  54. context.system.shutdown()
  55. }
  56. }
  57. def calculate(nrOfWorkers: Int, nrOfElements: Int, nrOfMessages: Int) {
  58. // Create an Akka system
  59. val system = ActorSystem("PiSystem")
  60. // create the result listener, which will print the result and shutdown the system
  61. val listener = system.actorOf(Props[Listener], name = "listener")
  62. // create the master
  63. val master = system.actorOf(Props(new Master(
  64. nrOfWorkers, nrOfMessages, nrOfElements, listener)),
  65. name = "master")
  66. // start the calculation
  67. master ! Calculate
  68. }
  69. }

作为命令行程序运行

如果你是手动输入(或者拷贝粘贴)指南中的代码到$AKKA_HOME/akka-tutorials/akka-tutorial-first/src/main/scala/akka/tutorial/first/scala/Pi.scala那么现在轮到你了. 开启一个shell,并进入akka安装 (cd$AKKA_HOME).

首先我们需要编译源码文件。使用Scala编译器scalac. 我们的应用依赖于akka-actor-2.0.jarJAR包 , 所以编译时将它加入到编译器的classpath中.

Linux/Unix/Mac 系统:

 
     
  1. $ scalac -cp lib/akka/akka-actor-2.0.jar Pi.scala

Windows 系统:

 
     
  1. C:\Users\jboner\src\akka\akka-2.0> scalac -cp lib\akka\akka-actor-2.0.jar Pi.scala

编译完就可以运行了。使用java来运行但是同样,我们要先将akka-actor-2.0.jarJAR 包加入到 classpath, 而这一次还需要添加Scala运行时库scala-library.jar和我们自己的代码编译出的class.

Linux/Unix/Mac 系统:

 
    
  1. $ java \
  2. -cp lib/scala-library.jar:lib/akka/akka-actor-2.0.jar:. \
  3. akka.tutorial.first.scala.Pi
  4. Pi approximation: 3.1415926435897883
  5. Calculation time: 359 millis

Windows 系统:

 
    
  1. C:\Users\jboner\src\akka\akka-2.0> java \
  2. -cp lib/scala-library.jar;lib\akka\akka-actor-2.0.jar;. \
  3. akka.tutorial.first.scala.Pi
  4. Pi approximation: 3.1415926435897883
  5. Calculation time: 359 millis

Ok!它能跑了。

在SBT中运行

如果你使用SBT,那么可以直接在SBT中运行本程序。先进行编译:

Linux/Unix/Mac 系统:

 
     
  1. $ sbt
  2. > compile
  3. ...

Windows 系统:

 
     
  1. C:\Users\jboner\src\akka\akka-2.0> sbt
  2. > compile
  3. ...

以上完成后直接在SBT中运行:

 
    
  1. > run
  2. ...
  3. Pi approximation: 3.1415926435897883
  4. Calculation time: 359 millis

Ok!它能跑了。

从外部修改配置 (可选)

示例项目的resources目录下包含一个application.conf文件:

 
    
  1. akka.actor.deployment {
  2. /master/workerRouter {
  3. # 取消下面两行的注释来修改计算过程,使用10个工作actor,而不是4个:
  4. #router = round-robin
  5. #nr-of-instances = 10
  6. }
  7. }

如果你取消那两行的注释,你应该会看到性能上的变化,基本上应该是更好的性能(你可能需要增加代码中的消息数量来延长应用的运行时间)。需要提醒注意的是修改的配置只在给出了路由器类型的时候才有效,所以仅取消nr-of-instances的注释将不起作用; 参阅Routing (Scala)了解细节.

注意

确保application.conf在运行应用的classpath上。如果在SBT中运行,那么这条件应该已经满足了,否则你需要把包含该文件的目录加入到jvm的-classpath参数.

总结

我们已经学习了如何创建第一个Akka项目,使用Akka actor来扩展到多核cpu上(也称为垂直扩展),为cpu密集型计算进行加速。我们还学习了在命令行上或在SBT中编译和运行Akka项目的方法。

如果你有一个多核的电脑,我建议你通过修改nrOfWorkers来尝试不同数量的工作actor来观察性能上的改进。

开发快乐 !


你可能感兴趣的:(入门指南 (Scala))