【AKKA 官方文檔翻譯】第一部分:Actor架構

第一部分: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

當我們停止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 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。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章