原文: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 系统:
- $ git clone git://github.com/akka/akka.git
- $ cd akka/akka-tutorials/akka-tutorial-first
Windows 系统:
- C:\Users\jboner\src> git clone git://github.com/akka/akka.git
- 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 系统:
- $ export JAVA_HOME=..root of Java distribution..
- $ export PATH=$PATH:$JAVA_HOME/bin
检测java正确安装:
- $ java -version
- java version "1.6.0_24"
- Java(TM) SE Runtime Environment (build 1.6.0_24-b07-334-10M3326)
- Java HotSpot(TM) 64-Bit Server VM (build 19.1-b02-334, mixed mode)
Windows 系统:
- C:\Users\jboner\src\akka> set JAVA_HOME=..root of Java distribution..
- C:\Users\jboner\src\akka> set PATH=%PATH%;%JAVA_HOME%/bin
检测java正确安装:
- C:\Users\jboner\src\akka> java -version
- java version "1.6.0_24"
- Java(TM) SE Runtime Environment (build 1.6.0_24-b07-334-10M3326)
- 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 系统:
- $ cd /Users/jboner/tools/akka-2.0
- $ export AKKA_HOME=`pwd`
- $ echo $AKKA_HOME
- /Users/jboner/tools/akka-2.0
Windows 系统:
- C:\Users\jboner\src\akka> cd akka-2.0
- C:\Users\jboner\src\akka\akka-2.0> set AKKA_HOME=%cd%
- C:\Users\jboner\src\akka\akka-2.0> echo %AKKA_HOME%
- C:\Users\jboner\src\akka\akka-2.0
安装路径的内容如下:
Linux/Unix/Mac 系统:
- $ ls -1
- bin
- config
- deploy
- doc
- lib
- src
Windows 系统:
- C:\Users\jboner\src\akka\akka-2.0> dir
- bin
- config
- deploy
- doc
- lib
- 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 版本. 如果你选择的是tgz或zip包则需要进行解压。如果选择的是IzPack安装程序,只需要双击它然后按照指示操作。
还必须保证scala-2.9.1/bin(scala-2.9.1是你的Scala安装目录) 在你的PATH上。
Linux/Unix/Mac 系统:
- $ export PATH=$PATH:scala-2.9.1/bin
Windows 系统:
- C:\Users\jboner\src\akka\akka-2.0> set PATH=%PATH%;scala-2.9.1\bin
你可以运行scala命令来测试你的安装是否正确。
Linux/Unix/Mac 系统:
- $ scala -version
- Scala code runner version 2.9.1.final -- Copyright 2002-2011, LAMP/EPFL
Windows 系统:
- C:\Users\jboner\src\akka\akka-2.0> scala -version
- 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文件:
- name := "My Project"
- version := "1.0"
- scalaVersion := "2.9.1"
- resolvers += "Typesafe Repository" at "http://repo.typesafe.com/typesafe/releases/"
- libraryDependencies += "com.typesafe.akka" % "akka-actor" % "2.0"
再创建一个名为src/main/scala的目录来存放源码.
虽然本指南并不需要,但是你可能愿意添加除了akka-actor外的其它Akka模块, 这些是添加在build.sbt的libraryDependencies部分。 注意其中的每一项之间必须有一个空行。下面是一个添加akka-remote的例子:
- libraryDependencies += "com.typesafe.akka" % "akka-actor" % "2.0"
- libraryDependencies += "com.typesafe.akka" % "akka-remote" % "2.0"
好,现在我们都弄好了。
SBT 自身有一堆依赖,不过我们的项目只需要其中一个:akka-actor-2.0.jar. SBT 会下载它。
开始编写代码
终于可以开始写代码了。
我们先创建Pi.scala文件并在文件顶部添加以下这些 import:
- import akka.actor._
- import akka.routing.RoundRobinRouter
- import akka.util.Duration
- 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以防止在我们不可控的地方创建消息):
- sealed trait PiMessage
- case object Calculate extends PiMessage
- case class Work(start: Int, nrOfElements: Int) extends PiMessage
- case class Result(value: Double) extends PiMessage
- case class PiApproximation(pi: Double, duration: Duration)
创建工作 actor
现在我们来创建工作 actor。 方法是混入Actortrait 并定义其中的receive方法.receive方法定义我们的消息处理器。我们让它能够处理Work消息,所以添加一个针对这种消息的处理器:
- class Worker extends Actor {
- // calculatePiFor ...
- def receive = {
- case Work(start, nrOfElements) ⇒
- sender ! Result(calculatePiFor(start, nrOfElements)) // perform the work
- }
- }
可以看到我们现在创建了一个Actor和一个receive方法作为Work消息的处理器. 在这个处理器中我们调用calculatePiFor(..)方法, 将结果包在Result消息里并使用sender异步发送回消息的原始发送者。 在Akka里,sender引用是与消息一起隐式发送的,这样接收者可以随时回复或将sender引用保存起来以备将来使用。
现在在我们的Workeractor 中唯一缺少的就是实现calculatePiFor(..)方法。 虽然在Scala里我们可以有很多方法来实现这个算法,在这个入门指南中我们选择了一种命令式的风格,使用了for写法和一个累加器:
- def calculatePiFor(start: Int, nrOfElements: Int): Double = {
- var acc = 0.0
- for (i ← start until (start + nrOfElements))
- acc += 4.0 * (1 - (i % 2) * 2) / (2 * i + 1)
- acc
- }
创建主actor
主actor会稍微复杂一些。 在它的构造方法里我们创建一个round-robin的路由器来简化将工作平均地分配给工作actor们的过程,先做这个:
- val workerRouter = context.actorOf(
- Props[Worker].withRouter(RoundRobinRouter(nrOfWorkers)), name = "workerRouter")
现在我们有了一个路由,可以在一个单一的抽象中表达所有的工作actor。现在让我们创建主actor. 传递给它三个整数变量:
- nrOfWorkers– 定义我们会启动多少工作actor
- nrOfMessages– 定义会有多少整数段发送给工作actor
- nrOfElements– 定义发送给工作actor的每个整数段的大小
下面是主actor:
- class Master(nrOfWorkers: Int, nrOfMessages: Int, nrOfElements: Int, listener: ActorRef)
- extends Actor {
- var pi: Double = _
- var nrOfResults: Int = _
- val start: Long = System.currentTimeMillis
- val workerRouter = context.actorOf(
- Props[Worker].withRouter(RoundRobinRouter(nrOfWorkers)), name = "workerRouter")
- def receive = {
- // handle messages ...
- }
- }
有一些需要进一步解释的事。
注意我们向主actor传进了一个ActorRef. 这是用来向外界报告最终的计算结果。
但是还没完。我们还缺少主actor的消息处理器. 这个处理器需要能够对两种消息进行响应:
- Calculate– 用来启动计算过程
- Result– 用来汇总不同的计算结果
Calculate处理器会通过其路由器向所有的工作actor 发送工作内容.
Result处理器从Result消息中获取值并汇总到我们的pi成员变量中. 我们还会记录已经接收的结果数据的数量,它是否与发送出去的任务数量一致 。主actor 发现计算完成了,会将最终结果发送给监听者. 当整个过程都完成了,它会调用context.stop(self)方法来终止自己和它所监管的所有actor. 在本例中,主actor监管一个actor,我们的路由器,而路由器监管着所有nrOfWorkers个工作actors. 所有的actor都会在其监管者的stop方法被调用时自动终止,并会传递给所有它监管的子actor。
让我们在代码中实现这些:
- def receive = {
- case Calculate ⇒
- for (i ← 0 until nrOfMessages) workerRouter ! Work(i * nrOfElements, nrOfElements)
- case Result(value) ⇒
- pi += value
- nrOfResults += 1
- if (nrOfResults == nrOfMessages) {
- // Send the result to the listener
- listener ! PiApproximation(pi, duration = (System.currentTimeMillis - start).millis)
- // Stops this actor and all its supervised children
- context.stop(self)
- }
- }
创建计算结果监听者
监听者很简单,当它接收到从Master发来的PiApproximation,就将结果打印出来并关闭整个Actor系统。
- class Listener extends Actor {
- def receive = {
- case PiApproximation(pi, duration) ⇒
- println("\n\tPi approximation: \t\t%s\n\tCalculation time: \t%s"
- .format(pi, duration))
- context.system.shutdown()
- }
- }
启动计算
现在只剩下实现启动和运行计算的执行者了。我们创建一个调用Pi的对象, 这里我们可以继承Scala中的Apptrait, 这个trait使我们能够在命令行上直接运行这个应用.
Pi对象是我们的actor和消息的很好的容器。所以我们把它们都放在这儿。我们还创建一个calculate方法来启动主actor 并等待它结束:
- object Pi extends App {
- calculate(nrOfWorkers = 4, nrOfElements = 10000, nrOfMessages = 10000)
- // actors and messages ...
- def calculate(nrOfWorkers: Int, nrOfElements: Int, nrOfMessages: Int) {
- // Create an Akka system
- val system = ActorSystem("PiSystem")
- // create the result listener, which will print the result and shutdown the system
- val listener = system.actorOf(Props[Listener], name = "listener")
- // create the master
- val master = system.actorOf(Props(new Master(
- nrOfWorkers, nrOfMessages, nrOfElements, listener)),
- name = "master")
- // start the calculation
- master ! Calculate
- }
- }
以上的calculate方法创建一个Actor系统,这是包括所有创建出的actor的 “上下文”。 如何在容器中创建actor的例子在calculate方法的‘system.actorOf(...)’这一行。 这里我们创建两个顶级actor. 如果你是在一个actor上下文(i.e. 在一个创建其它actor的actor中),你应该使用context.actorOf(...). 这在以上的主actor代码中有所体现。
好了,终于完成了。
但是在打包和运行之前,让我们看看完整的代码,包括package定义,import:
- /**
- * Copyright (C) 2009-2012 Typesafe Inc.
- */
- package akka.tutorial.first.scala
- import akka.actor._
- import akka.routing.RoundRobinRouter
- import akka.util.Duration
- import akka.util.duration._
- object Pi extends App {
- calculate(nrOfWorkers = 4, nrOfElements = 10000, nrOfMessages = 10000)
- sealed trait PiMessage
- case object Calculate extends PiMessage
- case class Work(start: Int, nrOfElements: Int) extends PiMessage
- case class Result(value: Double) extends PiMessage
- case class PiApproximation(pi: Double, duration: Duration)
- class Worker extends Actor {
- def calculatePiFor(start: Int, nrOfElements: Int): Double = {
- var acc = 0.0
- for (i ← start until (start + nrOfElements))
- acc += 4.0 * (1 - (i % 2) * 2) / (2 * i + 1)
- acc
- }
- def receive = {
- case Work(start, nrOfElements) ⇒
- sender ! Result(calculatePiFor(start, nrOfElements)) // perform the work
- }
- }
- class Master(nrOfWorkers: Int, nrOfMessages: Int, nrOfElements: Int, listener: ActorRef)
- extends Actor {
- var pi: Double = _
- var nrOfResults: Int = _
- val start: Long = System.currentTimeMillis
- val workerRouter = context.actorOf(
- Props[Worker].withRouter(RoundRobinRouter(nrOfWorkers)), name = "workerRouter")
- def receive = {
- case Calculate ⇒
- for (i ← 0 until nrOfMessages) workerRouter ! Work(i * nrOfElements, nrOfElements)
- case Result(value) ⇒
- pi += value
- nrOfResults += 1
- if (nrOfResults == nrOfMessages) {
- // Send the result to the listener
- listener ! PiApproximation(pi, duration = (System.currentTimeMillis - start).millis)
- // Stops this actor and all its supervised children
- context.stop(self)
- }
- }
- }
- class Listener extends Actor {
- def receive = {
- case PiApproximation(pi, duration) ⇒
- println("\n\tPi approximation: \t\t%s\n\tCalculation time: \t%s"
- .format(pi, duration))
- context.system.shutdown()
- }
- }
- def calculate(nrOfWorkers: Int, nrOfElements: Int, nrOfMessages: Int) {
- // Create an Akka system
- val system = ActorSystem("PiSystem")
- // create the result listener, which will print the result and shutdown the system
- val listener = system.actorOf(Props[Listener], name = "listener")
- // create the master
- val master = system.actorOf(Props(new Master(
- nrOfWorkers, nrOfMessages, nrOfElements, listener)),
- name = "master")
- // start the calculation
- master ! Calculate
- }
- }
作为命令行程序运行
如果你是手动输入(或者拷贝粘贴)指南中的代码到$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 系统:
- $ scalac -cp lib/akka/akka-actor-2.0.jar Pi.scala
Windows 系统:
- 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 系统:
- $ java \
- -cp lib/scala-library.jar:lib/akka/akka-actor-2.0.jar:. \
- akka.tutorial.first.scala.Pi
- Pi approximation: 3.1415926435897883
- Calculation time: 359 millis
Windows 系统:
- C:\Users\jboner\src\akka\akka-2.0> java \
- -cp lib/scala-library.jar;lib\akka\akka-actor-2.0.jar;. \
- akka.tutorial.first.scala.Pi
- Pi approximation: 3.1415926435897883
- Calculation time: 359 millis
Ok!它能跑了。
在SBT中运行
如果你使用SBT,那么可以直接在SBT中运行本程序。先进行编译:
Linux/Unix/Mac 系统:
Windows 系统:
- C:\Users\jboner\src\akka\akka-2.0> sbt
- > compile
- ...
以上完成后直接在SBT中运行:
- > run
- ...
- Pi approximation: 3.1415926435897883
- Calculation time: 359 millis
Ok!它能跑了。
从外部修改配置 (可选)
示例项目的resources目录下包含一个application.conf文件:
- akka.actor.deployment {
- /master/workerRouter {
- # 取消下面两行的注释来修改计算过程,使用10个工作actor,而不是4个:
- #router = round-robin
- #nr-of-instances = 10
- }
- }
如果你取消那两行的注释,你应该会看到性能上的变化,基本上应该是更好的性能(你可能需要增加代码中的消息数量来延长应用的运行时间)。需要提醒注意的是修改的配置只在给出了路由器类型的时候才有效,所以仅取消nr-of-instances的注释将不起作用; 参阅Routing (Scala)了解细节.
注意
确保application.conf在运行应用的classpath上。如果在SBT中运行,那么这条件应该已经满足了,否则你需要把包含该文件的目录加入到jvm的-classpath参数.
总结
我们已经学习了如何创建第一个Akka项目,使用Akka actor来扩展到多核cpu上(也称为垂直扩展),为cpu密集型计算进行加速。我们还学习了在命令行上或在SBT中编译和运行Akka项目的方法。
如果你有一个多核的电脑,我建议你通过修改nrOfWorkers来尝试不同数量的工作actor来观察性能上的改进。
开发快乐 !