第一部分: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.sampleimport akka.actor.{ Actor, Props, ActorSystem }import scala.io.StdInclass 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
當我們停止firstactor時,它在自己被關閉前關閉了它的子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 startedsupervised actor fails nowsupervised actor stoppedsupervised 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。