Scala Actor
简介
Actor提供了并发程序中与传统的基于锁的结构不同的另一种选择。通过尽可能避免锁和共享状态,Actor使得我们能够更加容易设计出正确、没有死锁或争用状态的程序。Scala类库提供了一个Actor模型的简单实现,除此之外还有其他更高级的Actor类库,比如Akka(http://akka.io)。
在 actor 并发模型中, actor 是独立的软件实体, 它们之间并不共享任何可变状态信息。 actor 之间无须共享信息, 通过交换消息的方式便可进行通
信。 通过消除同步访问那些共享可变状态, 编写健壮的并发应用程序变得非常简单。 尽管这些 actor 也许需要修改状态, 但是假如这些可变状态对
外不可访问, 并且 actor 框架确保 actor 相关代码调用是线程安全的, 开发者就无须再费力编写枯燥而又容易出错的同步原语 (synchronization primitive) 了
创建和启动Actor
创建Actor
//继承Actor特质类 class HiActor extends Actor { //scala.actors.Actor //重写Actor特质类的抽象方法act,从而实现自己的业务逻辑 override def act(): Unit = { //类似于Java中的Runnable接口的run方法 while (true)//消息循环 //使用case模式匹配接收到的消息 receive { case "Hi" => println("Hello") } } }
val actor1 = new HiActor actor1.start()
如果你需要临时创建actor而不是定义一个类。Actor伴生对象带有一个actor方法来创建和启动actor:
valactor2 = actor { while (true) { receive { case "Hi" => println("Hello") } } } }
case class Person(val name: String, val age: Int) class HiActor extends Actor { override def act(): Unit = { while (true) //消息循环 receive { //case模式匹配接收到的消息 case Person(name, age) => println("this is " + name + " : age is " + age) case Person("Alice",20) => println("OK") } /*发送到actor的消息被存放在一个“邮箱”中 receive方法从邮箱获取下一条消息并将它传递给它的参数,该参数是一个偏函数({ case xxxxxx})。 */ } } object exam1 { def main(args: Array[String]): Unit = { val actor1 = new HiActor actor1.start() //向actor1发送消息 actor1 ! Person("Alice", 20) actor1 ! Person("Bob", 29) actor1 ! Person("Cindy", 16) } }
· 消息投递的过程是异步的,它们会以什么样的顺序到达并不确定,所以在设计的时候要考虑到这一点,让程序不要依赖任何特定的消息投递顺序。
· 如果receive方法被调用时并没有消息,则该调用会阻塞,直到有消息抵达。
· 如果邮箱中没有任何消息可以被偏函数处理,则对receive方法的调用也会阻塞,直到一个可以匹配的消息抵达。
· 邮箱可能被那些不与任何case语句匹配的消息占满。你可以添加一个case _ 语句来处理任意的消息。
· 邮箱会串行化消息。Actor运行在单个线程中。它会先接收一条消息,然后接收下一条消息。不过在actor的代码中不用担心争用状况。
向其他Actor发送消息
当运算被分拆到不同actor来并行处理问题的各个部分时,这些处理结果需要被收集到一起。Actor可以将结果存入到一个线程安全的数据结构当中,比如一个并发的哈希映射,但actor模型并不鼓励使用共享数据。因而当actor计算出结果后,应该向另一个actor发送消息。
Actor计算结果发送方向:
1. 可以是一些全局的actor。不过,当actor数量很多时,该方案伸缩性并不好。
2. Actor可以构造成带有指向另一个或多个actor的引用。
3. Actor可以接收带有指向另一个actor的引用消息。
4. Actor可以返回消息给发送方。
消息通道
你可以通过共享消息通道来实现actor之间消息通信。
这样做的好处:
· 消息通道是类型安全的——你只能发送或接收特定的类型消息。
· 你不会不小心通过消息通道调用到某个actor方法。
消息通道可以是一个OutputChannel(带 ! 的方法),也可以是一个InputChannel(带receive或react方法)。Channel类同时扩展了OutputChannel和InputChannel特质。
要构造一个消息通道,你可以提供一个actor:
1. val channel = new Channel[Int](actor1)
如果不提供构造参数,那么消息通道就会绑定到当前执行的这个actor上。
代码示例:
import scala.actors.{Actor, Channel, OutputChannel} import scala.actors.Actor._ case class Compute(input: Seq[Int], result: OutputChannel[Int]) class Computer extends Actor { override def act(): Unit = { while (true) { receive { case Compute(input, out) => { val answer = 88 out ! answer } } } } } object exam1 { def main(args: Array[String]): Unit = { val actor1 = actor { val channel = new Channel[Int] val computeActor: Computer = new Computer val input: Seq[Int] = List(11, 22, 33) computeActor.start() computeActor ! Compute(input, channel) channel.receive { case x: Int => println("result is " + x) case _ => //xx } } actor1.start() } }
同步消息
actor可以发送一个消息并等待回复,用 !? 操作符即可。
import scala.actors.Actor case class Person(val name: String, val age: Int) class HiActor extends Actor { override def act(): Unit = { while (true) receive { case Person(name, age) => { println("this is " + name + " : age is " + age) sender ! "replied message..." } } } } object exam1 { def main(args: Array[String]): Unit = { val actor1 = new HiActor actor1.start() //向actor1发送消息并等待回复,用 !? 操作符即可。 val reply = actor1 !? Person("Alice", 20) println(reply) //"replied message..." } }
这两种返回效果相同:
1. sender ! "replied message..."
2. reply("replied message...")
同步消息很容易引发死锁。通常而言,最好避免在actor的act方法里执行阻塞调用。
future
除了等待对方返回结果之外,你也可以选择接收一个future——这是一个将在结果可用时产出结果的对象。使用 !! 方法:
1. val replyFuture = acount !! Deposite(1000)
isSet方法会检查结果是否已经可用。要接收该结果,使用函数调用的表示方法:
1. val reply = replyFuture()
这个调用将会阻塞,直到回复被发送。
共享线程
某些程序包含的actor过多,以至于要为actor创建单独的线程开销会很大。此时需要考虑在同一个线程中运行多个actor。Actor有时大部分时间用户等待消息,这时actor所在的单独线程会堵塞,与其这样不如用一个线程来执行多个actor的消息处理函数。
在Scala中,react方法可以实现这样的功能。react方法接收一个偏函数,并将它添加到邮箱,然后退出。
react工作原理
当你调用一个actor的start时,start方法会以某种方式来确保最终会有某个线程来调用那个actor的act方法。如果act方法调用了react,则react方法会在actor的邮箱中查找传递给偏函数的能够处理的消息。(和receive方法一样,传递待处理消息给偏函数的isDefinedAt方法。)如果找到一个可以处理的消息,react 会安排一个在未来某个时间处理该消息的计划并抛出异常。如果它没有找到这样的消息,它会将actor置于“冷存储” 状态 ,在它通过邮箱收到更多消息时重新激活,并抛出异常。不论是哪种情况,react都会以这个异常的方式完成其执行,act 方法也随之结束 调用act的线程会捕获这个异常,忘掉这个actor , 并继续处理其他事务。这就是为什么你想要react在处理第一个消息之外做更多的事,你将需要在偏函数中再次调用act方法,或使用某种其他的手段来让react再次被调用。
较为复杂的代码示例
import java.net.InetAddress import java.net.UnknownHostException import actors._, Actor._ import scala.actors.Actor case class Net(name: String, actor: Actor) /** * scala Actor构建在java的线程基础之上的, * 为了避免频繁的线程创建、销毁和切换等,scala中提供了react方法 * 方法执行完毕后,仍然被保留 */ object NameResolver extends Actor { def act() { react { //模式匹配 case Net(name, actor) => /**向actor发送解析后的IP地址 * 该例中,actor为本身,即向本身发送消息,存入邮箱 */ actor ! getIpAddress(name) //再次调用act方法,从本身邮箱中读取消息 //如果消息为空,则等待 act case "EXIT" => println("Name resolver exiting.") //匹配邮箱中的单个信息,本例中会匹配邮箱中的IP地址信息 case msg => println("Unhandled message: " + msg) act } } def getIpAddress(name: String): Option[InetAddress] = { try { println("2: "+InetAddress.getByName(name)) Some(InetAddress.getByName(name)) } catch { case _: UnknownHostException => None } } } object ActorWithReAct extends App { //启动Actor NameResolver.start() //发消息 NameResolver ! Net("www.baidu.com", self) //接收消息 println("1: "+self.receive { case msg => msg }) }
//2: www.baidu.com/180.97.33.108
//1: Some(www.baidu.com/180.97.33.108)
Actor生命周期
actor的act方法在actor的start方法被调用的时候开始执行。通常,actor接下来做的事情是进入某个循环,例如:
def act(): Unit ={
while (...){
receive{
//
}
}
7. }
actor在如下情形之一会终止执行:
· Act方法返回
· Act方法由于异常被终止
· Actor调用exit方法
exit方法是个受保护的方法。它只能被Actor子类中调用。例如:
val actor2 = actor{
while(true){
receive{
case "Hi" => println("Hello")
case "Bye" => exit()
}
}
}
其他方法不能调用exit()来终止一个actor。
当actor因一个异常终止时,退出原因就是UncaughtException样例类的一个实例。该样例类有如下属性:
acotr: 抛出异常的actor。
· message: Some(msg) msg为该actor处理的最后一条消息;或者None,如果actor在没来得及处理任何消息之前就挂掉的话。
· Sender:Some(channel) channel代表最后一条消息的发送方的输出消息通道;或者None,如果actor在没来得及处理任何消息之前就挂掉。
· Thread: actor退出时所在的线程。
· Cause: 相应的异常。
总结
1. 每个actor都要扩展Actor类并提供act方法。
2. 要往actor发送消息,可以用actor !Message。
3. 消息发送是异步的:“发完就忘”
4. 要接收消息,actor可以调用receive或react,通常是在循环中这样做。
5. receive/react的参数是由case语句组成的代码块(是一个偏函数)
6. 不同actor之间不应该共享状态。总是使用消息来发送数据。
7. 不要直接调用actor方法。通过消息进行通信。
8. 避免同步消息——也就是说将发送消息和等待响应分开。
9. 不同actor可以通过react而不是receive来共享线程,前提是消息处理器的控制流转足够简单。
10. 让actor挂掉是可以的,前提是你有其他actor监控着actor生死。用链接来设置监控关系。
akka
Scala 已经将原先的 actor 库从类库中移除, 而将 Akka 视为处理基于 actor 并发模型的官方类库。 Akka 是一个独立项目。 Scala 和 Akka 都由 Typesafe ( http://typesafe.com ) 开发并提供支持的, 同时 Akka 也提供了一套完整的 Java API 。import akka.actor.Actor import akka.actor.ActorSystem import akka.actor.Props class HelloActor extends Actor { def receive = { case "hello" => println("您好!") case _ => println("您是?") } } object Main extends App { val system = ActorSystem("HelloSystem") // 缺省的Actor构造函数 val helloActor = system.actorOf(Props[HelloActor], name = "helloactor") //返回类型是ActorRef helloActor ! "hello" helloActor ! "喂" }
//#full-example import akka.actor.{Actor, ActorRef, ActorSystem, Props} //ActorRef是actor的引用 //#greeter-companion //#greeter-messages object Greeter { //#greeter-messages def props(message: String, printerActor: ActorRef): Props = Props(new Greeter(message, printerActor)) //#greeter-messages final case class WhoToGreet(who: String) case object Greet } //#greeter-messages //#greeter-companion //#greeter-actor class Greeter(message: String, printerActor: ActorRef) extends Actor { import Greeter._ import Printer._ var greeting = "" def receive = { case WhoToGreet(who) => greeting = s"$message, $who" case Greet => //#greeter-send-message printerActor ! Greeting(greeting) //#greeter-send-message } } //#greeter-actor //#printer-companion //#printer-messages object Printer { //#printer-messages def props: Props = Props[Printer] //#printer-messages final case class Greeting(greeting: String) } //#printer-messages //#printer-companion //#printer-actor class Printer extends Actor { import Printer._ def receive = { case Greeting(greeting) => println(s"Greeting received (from ${sender}): $greeting") } } //#printer-actor //#main-class object AkkaQuickstart extends App { import Greeter._ // Create the 'helloAkka' actor system val system: ActorSystem = ActorSystem("helloAkka") //是actor创建工厂 //#create-actors // Create the printer actor val printer: ActorRef = system.actorOf(Printer.props, "printerActor") // Create the 'greeter' actors val howdyGreeter: ActorRef = system.actorOf(Greeter.props("Howdy", printer), "howdyGreeter") //第一个参数是Props,用于创建actor val helloGreeter: ActorRef = system.actorOf(Greeter.props("Hello", printer), "helloGreeter") val goodDayGreeter: ActorRef = system.actorOf(Greeter.props("Good day", printer), "goodDayGreeter") //#create-actors //#main-send-messages howdyGreeter ! WhoToGreet("Akka") howdyGreeter ! Greet howdyGreeter ! WhoToGreet("Lightbend") howdyGreeter ! Greet helloGreeter ! WhoToGreet("Scala") helloGreeter ! Greet goodDayGreeter ! WhoToGreet("Play") goodDayGreeter ! Greet //#main-send-messages } //#main-class //#full-example
import akka.actor._ case object PingMessage case object PongMessage case object StartMessage case object StopMessage class Ping(pong: ActorRef) extends Actor { var count = 0 def incrementAndPrint { count += 1; println("ping") } def receive = { case StartMessage => incrementAndPrint pong ! PingMessage case PongMessage => if (count > 9) { sender ! StopMessage println("ping stopped") context.stop(self) } else { incrementAndPrint sender ! PingMessage } } } class Pong extends Actor { def receive = { case PingMessage => println(" pong") sender ! PongMessage case StopMessage => println("pong stopped") context.stop(self) context.system.shutdown() } } object PingPongTest extends App { val system = ActorSystem("PingPongSystem") val pong = system.actorOf(Props[Pong], name = "pong") val ping = system.actorOf(Props(new Ping(pong)), name = "ping") // start them going ping ! StartMessage }