第一部分:Actor架構
akka版本2.5.8
版權聲明:本文爲博主原創文章,未經博主允許不得轉載。
Akka爲你提供了創建actor系統的一些基礎功能,並且已經實現了控制基本行爲必需的底層代碼。爲了體會這一點,我們來看看你創建的actor角色與akka內部創建的管理角色的關係,順便了解下actor的生命週期和失敗處理方式。
akka actor的層次結構
akka裏的actor總是屬於其父母。通常你通過調用 context.actorOf()
創建一個actor。這種方式向現有的actor樹內加入了一個新的actor,這個actor的創建者就成爲了這個actor的父actor。你可能會問,誰是你創建的第一個actor的父actor呢?
如下圖所示,所有actor都有一個共同的家長,稱爲user guardian。可以通過調用 system.actorOf()
來創建屬於它新actor實例。正如我們在快速入門指南中所介紹的,創建actor將返回一個有效的URL的引用,因此,如果我們通過調用system.actorOf(…, "someActor")
創建一個名爲someActor
的actor時,其引用將包含路徑/user/someActor
。
實際上在你調用你創建actor的代碼之前,akka已經在系統中創建了三個actor。這些內置的actor名字都包含guardian,因爲它們會監管它們各自路徑下的所有子actor。guardian actor包括:
/
所謂的根監護人。這是系統中所有actor的父親,當系統被終止時,它也是最後一個被停止的。
/user
這是所有用戶創建的actor的父親。不要被user這個名字所迷惑,他與最終用戶沒有關係,也和用戶處理無關。你使用akka庫所創建的所有actor的路徑都將以/user/
開頭
/system
系統監護人
在Hello World示例中,我們已經看到了如何使用system.actorOf()
來創建一個在/user
路徑下的actor。儘管它只是在用戶創建的層次的最高級actor,但是我們把它稱作頂級actor。在你的ActorSystem
裏,你通常只有一個(或者非常少)頂級actor。我們在已有的actor裏通過調用context.actorOf()
來創建新的非頂級actor,即子actor。context.actorOf()
的方法簽名和system.actorOf()
相同。
查看actor層級結構最簡單的方式就是打印ActorRef
的實例。在這個小實驗中,我們創建了一個actor,打印了它的引用,爲他創建了一個子actor,並打印其子actor的引用。我們從Hello World工程開始,如果你還沒有下載它,請從Lightbend Tech Hub下載Quickstart工程。
在你的Hello World工程裏,導航到com.lightbend.akka.sample
包的位置,在這裏創建一個新Scala文件ActorHierarchyExperiments.scala
。複製粘貼下面的代碼到這個新文件裏。保存文件並運行sbt "runMain com.lightbend.akka.sample.ActorHierarchyExperiments"
,然後觀察輸出。
package com.lightbend.akka.sample
import akka.actor.{ Actor, Props, ActorSystem }
import scala.io.StdIn
class PrintMyActorRefActor extends Actor {
override def receive: Receive = {
case "printit" ⇒
val secondRef = context.actorOf(Props.empty, "second-actor")
println(s"Second: $secondRef")
}
}
object ActorHierarchyExperiments extends App {
val system = ActorSystem("testSystem")
val firstRef = system.actorOf(Props[PrintMyActorRefActor], "first-actor")
println(s"First: $firstRef")
firstRef ! "printit"
println(">>> Press ENTER to exit <<<")
try StdIn.readLine()
finally system.terminate()
}
注意一點,即如何給第一個actor發送一個消息來使它開始工作。我們使用父類引用來發送信息:firstRef ! "printit"
。當程序運行的時候,輸出信息會包含第一個actor的引用,也包含在printit
中創建的子actor的引用。你的輸出應該看上去像這樣:
First: Actor[akka://testSystem/user/first-actor#1053618476]
Second: Actor[akka://testSystem/user/first-actor/second-actor#-1544706041]
請注意引用結構:
兩個路徑都以
akka://testSystem/
開頭,所有的actor引用都是合法的URL。akka://
是協議字段。接下來,就像在World Wide Web上一樣,URL是一個系統的標識。在這個示例裏,系統的名稱爲
testSystem
,它也可以被命名爲任意名字。如果開啓了多系統間的遠程通信,URL裏就會包含主機名,以便其他系統可以在網絡上找到它。因爲第二個actor引用的路徑裏包含
/first-actor/
,這表明它是第一個actor的子actor。actor引用的最後一段,
#1053618476
和#-1544706041
是一個actor的唯一標識符uid,你在大多數場景下可以忽略它的存在。
現在你似乎瞭解了actor系統的層次結構,你可能會想:爲什麼我們需要這個層次結構?他是幹什麼用的呢?
層次結構的一個很重要的功能是來安全地管理actor的生命週期,接下來讓我們思考一下怎麼利用它來寫出優秀的代碼。
actor的生命週期
actor在被創建後存在,並且在用戶請求關閉時消失。當actor被關閉後,其所有的子actor都將被遞歸地關閉。這個特性極大簡化了我們的資源清理工作,並且防止資源泄露(套接字或者文件等)。實際上,在進行底層多線程編程編程時,我們經常會小看對各種併發資源生命週期管理的難度。
爲了關閉actor,我們推薦在actor內部調用context.stop(self)
來自我關閉,這個代碼經常放在用戶定義的結束信息迴應裏,或者在actor已經做完了其工作後自行調用。停止其他的actor在技術上是允許的,可以通過調用context.stop(actorRef)
來實現。但是用這種方式來停止一個actor是一個壞習慣,但是你可以給這個actor發送PoisonPill
或者自定義關閉消息來關閉它。
Akka的actor提供了很對生命週期API,你可以在實現actor時重載這些方法。最常用的是preStart()
和postStop()
。
preStart()
會在actor啓動後,並在它處理第一個消息之前被調用
postStop()
會在actor將要被關閉前被調用,在它之後,actor不會再處理任何消息了
讓我們簡單實驗下如何使用preStart()
和postStop()
生命週期鉤子去觀察actor關閉時的行爲。首先,在你的工程裏添加以下兩個actor類:
class StartStopActor1 extends Actor {
override def preStart(): Unit = {
println("first started")
context.actorOf(Props[StartStopActor2], "second")
}
override def postStop(): Unit = println("first stopped")
override def receive: Receive = {
case "stop" ⇒ context.stop(self)
}
}
class StartStopActor2 extends Actor {
override def preStart(): Unit = println("second started")
override def postStop(): Unit = println("second stopped")
// Actor.emptyBehavior 是一個有用的佔位符
// 在我們不想用這個actor處理任何信息是使用它
override def receive: Receive = Actor.emptyBehavior
}
像之前一樣創建一個主類(main),負責創建actor並且在之後給它們發送"stop"
消息。
val first = system.actorOf(Props[StartStopActor1], "first")
first ! "stop"
你可以再次使用sbt
來啓動這個程序,得到的輸出應該是這樣的:
first started
second started
second stopped
first stopped
當我們停止first
actor時,它在自己被關閉前關閉了它的子actor:second
。這個順序是嚴格被執行的,所有子actor的postStop()
鉤子都將在它們的父actorpostStop()
調用之前被調用。
Akka參考手冊中Actor生命週期部分提供了有關整個生命週期鉤子的詳細信息。
失敗處理
父actor和子actor在整個聲明週期內都保持着聯繫。當一個actor失敗了(拋出異常或者在receive
裏冒出一個未處理的異常),他會被暫時地掛起。就像之前提到的一樣,失敗信息會被傳遞到父actor中,然後由父actor來決定如何處理這個子actor產生的異常。在這種方式下,父actor就是子actor的監管者,默認的監管策略就是停止並且重啓子actor。如果你沒有修改默認的監管策略,那麼所有的失敗都會導致重啓actor。
讓我們在一個簡單的實驗中來測試下默認的監管策略。就像之前一樣,在你的工程裏添加以下的類。
class SupervisingActor extends Actor {
val child = context.actorOf(Props[SupervisedActor], "supervised-actor")
override def receive: Receive = {
case "failChild" ⇒ child ! "fail"
}
}
class SupervisedActor extends Actor {
override def preStart(): Unit = println("supervised actor started")
override def postStop(): Unit = println("supervised actor stopped")
override def receive: Receive = {
case "fail" ⇒
println("supervised actor fails now")
throw new Exception("I failed!")
}
}
並且在mian裏用以下代碼運行:
val supervisingActor = system.actorOf(Props[SupervisingActor], "supervising-actor")
supervisingActor ! "failChild"
你應該會看到類似如下輸出:
supervised actor started
supervised actor fails now
supervised actor stopped
supervised actor started
[ERROR] [03/29/2017 10:47:14.150] [testSystem-akka.actor.default-dispatcher-2] [akka://testSystem/user/supervising-actor/supervised-actor] I failed!
java.lang.Exception: I failed!
at tutorial_1.SupervisedActor$$anonfun$receive$4.applyOrElse(ActorHierarchyExperiments.scala:57)
at akka.actor.Actor$class.aroundReceive(Actor.scala:513)
at tutorial_1.SupervisedActor.aroundReceive(ActorHierarchyExperiments.scala:47)
at akka.actor.ActorCell.receiveMessage(ActorCell.scala:519)
at akka.actor.ActorCell.invoke(ActorCell.scala:488)
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 akka.dispatch.forkjoin.ForkJoinTask.doExec(ForkJoinTask.java:260)
at akka.dispatch.forkjoin.ForkJoinPool$WorkQueue.runTask(ForkJoinPool.java:1339)
at akka.dispatch.forkjoin.ForkJoinPool.runWorker(ForkJoinPool.java:1979)
at akka.dispatch.forkjoin.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:107)
我們看到在actor失敗後,被監管的actor被立即停止並重啓,我們也看到了一條這個異常被處理的日誌。在測試中,我們使用preStart()
和postStop()
鉤子,這些鉤子可以在actor被重啓前後被調用,所以我們不能用它來區分actor是第一次啓動還是被重啓。重啓actor在大多數情況是正確的,重啓可以讓actor恢復到一個已知的正確狀態上,也就是啓動時的乾淨狀態。在內部真正發生的是:preRestart()
和postRestart()
方法被調用,如果它們沒有被重載,則它們分別會調用postStop()
和preStart()
你可以嘗試重載這些方法,看看輸出的改變。
如果你已經不耐煩了,那麼你可以在監督參考頁面裏獲得更多深入的細節。
總結
我們已經學到了Akka是如何管理actor的,actor是有層級結構的,並且父actor監管它們的子actor,並處理子actor的異常。我們看到了如何創建一個非常簡單的actor和子actor。接下來,我們會通過通信建模將這些知識應用在我們的實例裏,以便從設備actor裏獲取信息。之後,我們會學到如何在組裏管理actor。