《Scala并发编程》Actor生命周期示例
1.Actor 故障重启过程
package chapter08
import akka.actor.{Actor, ActorRef, Props}
import akka.event.Logging
import chapter08.scala01.ourSystem
object scala01_1_6 extends App {
/**
* 8.1.6 角色的生命周期
*
* @前文中的ChildActor类重载了postStop方法,用于在角色终止时输出一些日志信息。
* @而本节将讨论postStop方法到底是在什么时机调用的,以及角色的生命周期里的其他重要事件。
*/
/**
* @为理解角色的生命周期的重要性,首先考虑一种情况,如果角色在处理消息时抛出一个异常,会发生什么?
* @在Akka中,这样的异常被视为正常行为,所以顶层的用户角色抛出异常时会默认重启。
* @重启会生产一个全新的角色对象,即角色状态会重新初始化。
* @当角色重启之后,它的角色引用和角色路径是保持不变的。
* @因而,同一个ActorRef对象只代表了逻辑意义下的同一个角色,其实际的物理角色对象有可能有多个。
* @--@这也是角色绝不允许它的this引用泄露的原因,否则程序其他部分就会引用到旧的角色对象,于是让角色引用不再具有透明性。
* @此外,暴露角色对象的this引用可能会暴露角色内部的实现细节,甚至会引起数据损坏。
* 绝不要将角色对象的this引用传给其他角色,因为这会破坏角色的封装。
*/
/**
* 下面开始讨论完整的角色的生命周期。
* @前文提到了,一个逻辑角色实例在调用actorOf方法时产生,
* @Props对象用于实例化一个物理角色对象。这个对象会被指定一个邮箱,并且可以开始接收消息。
* @actorOf方法向调用者返回一个角色引用,而所有的角色都可以并发地执行。
*/
/**
* @在角色开始处理消息之前,它的preStart方法会被调用,此方法用于初始化逻辑角色实例。
* @角色创建完毕之后,它开始处理消息。
* @在某个时刻,一个角色可能会因为异常而需要重启,这时,preRestart方法会被调用。
* @然后,所有的子角色会被停止。
* @随后,之前用于actorOf方法创建角色的那个Props对象会被重用,用于创建一个新的角色对象,
* @并在新的角色对象上调用其postRestart方法。
* @postRestart方法返回之后,新的角色对象的邮箱被指定为旧的角色对象的邮箱,它会继续处理重启之前邮箱中剩余的消息。
*/
/**
* @默认情况下,postRestart方法会调用preStart方法。
* @在有些情况下,用户可能想修改这种行为。
* @比如,数据库连接可能只需要在preStart中打开一次,并在逻辑角色实例停止时再关闭。
* @一旦逻辑角色实例要停止了,postStop方法会被调用,角色的角色路径会被释放,并返回给角色系统。
* @preRestart方法默认会调用postStop方法。完整角色的生命周期如图8.4所示。
*/
/**
* @注意,在角色的生命周期中,角色系统其他部分看到的是同一个角色引用,它不会因为角色重启而改变。
* @角色的失败和重启对角色系统其他部分而言是不可见的。
*/
/**
* @为测试一个角色的生命周期,下面声明两个角色类,即StringPrinter和LifecycleActor。
* @StringPrinter角色将它接收到的每个消息输出成日志。
* @而它的preStart和postStop方法会被重载,以精确地追踪角色启动和终止的时机,如下面的代码所示。
*/
class StringPrinter extends Actor {
val log = Logging(context.system, this)
def receive = {
case msg => log.info(s"[${this.toString}] printer got message '$msg'")
}
override def preStart(): Unit = log.info(s"[print-${this.hashCode()}] printer preStart.")
override def postStop(): Unit = log.info(s"[print-${this.hashCode()}] printer postStop.")
}
/**
* @LifecycleActor类维护了一个指向StringPrinter角色的子角色引用。
* @LifecycleActor类会响应Double和Int消息,并输出;
* @也可以响应List消息,并输出链表的第一个元素。
* @当LifecycleActor实例收到一个String消息时,它会将其转发给它的子角色。
*/
class LifecycleActor extends Actor {
val log = Logging(context.system, this)
var child: ActorRef = _
def receive = {
case num: Double => log.info(s"got a double - $num")
case num: Int => log.info(s"got an integer - $num")
case lst: List[_] => log.info(s"list - ${lst.head}, ...")
case txt: String => child ! txt
}
/**
* @下面要重载其生命周期钩子函数。首先是preStart方法,它用于输出一个日志,并实例化子角色。
* @这保证了子角色引用在子角色开始处理消息之前就初始化完成了。
*/
override def preStart(): Unit = {//和receive是同步的
log.info(s"[life-${this.hashCode()}] about to start")
child = context.actorOf(Props[StringPrinter], "kiddo")
}
/**
* @接下来重载preRestart和postRestart方法。在这两个方法中,会输出失败的原因。
* @postRestart方法默认会调用preStart方法,以便让新的角色对象在重启之后初始化得到一个新的子角色对象。
*/
override def preRestart(t: Throwable, msg: Option[Any]): Unit = {
log.info(s"[life-${this.hashCode()}] about to restart because of $t, during message $msg")
super.preRestart(t, msg)
}
override def postRestart(t: Throwable): Unit = {
log.info(s"[life-${this.hashCode()}] just restarted due to $t")
super.postRestart(t)
}
/**
* @重载postStop方法,用于追踪角色终止的时机。
*/
override def postStop() = log.info(s"[life${this.hashCode()}] just stopped")
/**
* @现在,创建一个LifecycleActor类的实例testy,并向其发送消息math.Pi。
* @此角色会在preStart方法中输出about-to-start的信息,创建一个新的子角色。
* @然后输出它接收到的消息math.Pi。
* @注意,该角色的about-to-start日志信息是在math.Pi消息被接收到之后输出来的。
* @这表明角色创建过程是异步操作:当调用actorOf时,创建出的角色会由角色系统代理,然后程序立即开始执行。
*/
}
val testy = ourSystem.actorOf(Props[LifecycleActor], "testy")
//Thread.sleep(2000)
//testy ! math.Pi
/**
* 向testy发送一个字符串消息,此消息会被转发给子角色,子角色接收到消息之后在日志记录中输出。
*/
//testy ! "hi there!"
/**
* @向testy发送Nil消息,此对象表示一个空链表,所以testy在尝试获取第一个元素时抛出一个异常。
* @这时角色汇报它需要重启,随后可以看到子角色也要重启的消息。
* @因为前文提到过,当一个角色重启时,子角色会被停止。
* @最后,testy会输出它将要重启的消息,而新的子角色也将初始化。这些事件都源自下面这条语句。
*/
testy ! Nil
/**
* @测试角色的生命周期揭示了actorOf方法的一个重要性质,即调用actorOf方法时,执行过程会继续,而不会等待角色充分初始化完毕。
* @类似地,发送一个消息不会阻塞执行过程,无论其他角色是否接收到或处理,执行过程都会继续。
* @这种消息称为异步消息。8.2节将介绍多种通信模式中是如何处理这种异步消息的。
*/
}
[INFO] [11/19/2022 22:28:13.388] [OurExampleSystem-akka.actor.default-dispatcher-4] [akka://OurExampleSystem/user/testy] [life-1225620711] about to start
[INFO] [11/19/2022 22:28:13.390] [OurExampleSystem-akka.actor.default-dispatcher-2] [akka://OurExampleSystem/user/testy/kiddo] [print-1656787776] printer preStart.
[ERROR] [11/19/2022 22:28:13.397] [OurExampleSystem-akka.actor.default-dispatcher-2] [akka://OurExampleSystem/user/testy] head of empty list
java.util.NoSuchElementException: head of empty list
at scala.collection.immutable.Nil$.head(List.scala:420)
at scala.collection.immutable.Nil$.head(List.scala:417)
at chapter08.scala01_1_6$LifecycleActor$$anonfun$receive$2.applyOrElse(scala01_1_6.scala:83)
at akka.actor.Actor$class.aroundReceive(Actor.scala:497)
at chapter08.scala01_1_6$LifecycleActor.aroundReceive(scala01_1_6.scala:77)
at akka.actor.ActorCell.receiveMessage(ActorCell.scala:526)
at akka.actor.ActorCell.invoke(ActorCell.scala:495)
at akka.dispatch.Mailbox.processMailbox(Mailbox.scala:257)
at akka.dispatch.Mailbox.run(Mailbox.scala:224)
at akka.dispatch.Mailbox.exec(Mailbox.scala:234)
at scala.concurrent.forkjoin.ForkJoinTask.doExec(ForkJoinTask.java:260)
at scala.concurrent.forkjoin.ForkJoinPool$WorkQueue.runTask(ForkJoinPool.java:1339)
at scala.concurrent.forkjoin.ForkJoinPool.runWorker(ForkJoinPool.java:1979)
at scala.concurrent.forkjoin.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:107)
[INFO] [11/19/2022 22:28:13.398] [OurExampleSystem-akka.actor.default-dispatcher-4] [akka://OurExampleSystem/user/testy] [life-1225620711] about to restart because of java.util.NoSuchElementException: head of empty list, during message Some(List())
[INFO] [11/19/2022 22:28:13.400] [OurExampleSystem-akka.actor.default-dispatcher-4] [akka://OurExampleSystem/user/testy] [life1225620711] just stopped
[INFO] [11/19/2022 22:28:13.401] [OurExampleSystem-akka.actor.default-dispatcher-3] [akka://OurExampleSystem/user/testy/kiddo] [print-1656787776] printer postStop.
[INFO] [11/19/2022 22:28:13.402] [OurExampleSystem-akka.actor.default-dispatcher-4] [akka://OurExampleSystem/user/testy] [life-1406877080] just restarted due to java.util.NoSuchElementException: head of empty list
[INFO] [11/19/2022 22:28:13.403] [OurExampleSystem-akka.actor.default-dispatcher-4] [akka://OurExampleSystem/user/testy] [life-1406877080] about to start
[INFO] [11/19/2022 22:28:13.403] [OurExampleSystem-akka.actor.default-dispatcher-2] [akka://OurExampleSystem/user/testy/kiddo] [print-32583356] printer preStart.
其中 1、2、3、4、5都是线性化的,postRestart方法返回之后,新的角色对象的邮箱被指定为旧的角色对象的邮箱,它会继续处理重启之前邮箱中剩余的消息。
对比线程的HashCode 值 可以确定actor更替时间
2.Actor 停止方法 context.stop
角色终止调用的是context.stop。调用上下文对象上的stop方法会让角色在处理完当前消息之后立刻停止。
可以看到调用了context.stop(self) 等待receive方法的完成后 再停止