JVM 併發性: 使用 Akka 執行非同步操作

developerworks發表於2015-08-09

本 系列 中以前的文章介紹瞭如何通過以下方式實現併發性:

  • 並行地在多個資料集上執行相同的操作(就像 Java 8 流一樣)
  • 顯式地將計算構建成非同步執行某些操作,然後將結果組合在一起(就像 future 一樣)。

這兩種方法都是實現併發性的不錯方式,但是您必須將它們明確地設計到應用程式中。

在本文和接下來的幾篇文章中,我將著重介紹一種不同的併發性實現方法,該方法基於一種特定的程式結構,與顯式編碼方法不同。這種程式結構就是 actor 模型。您將瞭解如何使用 actor 模型的 Akka 實現。(Akka 是一個構建併發和分散式 JVM 應用程式的工具包和執行時。)請參閱 參考資料,獲取本文完整示例程式碼的連結。

Actor 模型基礎知識

用於併發計算的 actor 模型基於各種稱為 actor 的原語來構建系統。Actor 執行操作來響應稱為訊息 的輸入。這些操作包括更改 actor 自己的內部狀態,以及發出其他訊息和建立其他 actor。所有訊息都是非同步交付的,因此將訊息傳送方與接收方分開。正是由於這種分離,導致 actor 系統具有內在的併發性:可以不受限制地並行執行任何擁有輸入訊息的 actor。

在 Akka 術語中,actor 看起來就像是某種通過訊息進行互動的行為神經束。像真實世界的演員一樣,Akka actor 也需要一定程度的隱私。您不能直接將訊息傳送給 Akka actor。相反,需要將訊息傳送給等同於郵政信箱的 actor 引用。然後通過該引用將傳入的訊息路由到 actor 的郵箱,以後再傳送給 actor。Akka actor 甚至要求所有傳入的訊息都是無菌的(或者在 JVM 術語中叫做不可變的),以免受到其他 actor 的汙染。

與一些真實世界中演員的需求不同,Akka 中由於某種原因而存在一些看似強制要求的限制。使用 actor 的引用可阻止交換訊息以外的任何互動,這些互動可能破壞 actor 模型核心上的解耦本質。Actor 在執行上是單執行緒的(不超過 1 個執行緒執行一個特定的 actor 例項),所以郵箱充當著一個緩衝器,在處理訊息前會一直儲存這些訊息。訊息的不可變性(由於 JVM 的限制,目前未由 Akka 強制執行,但這是一項既定的要求)意味著根本無需擔心可能影響 actor 之間各種共享的資料的同步問題;如果只有共享的資料是不可變的,那麼根本不需要同步。

開始實現

現在您已大體瞭解了 actor 模型和 Akka 細節,是時候看些程式碼了。使用 hello 作為編碼示例司空見慣,但它確實能夠幫助使用者快速、輕鬆地理解一種語言或系統。清單 1 顯示了 Scala 中的一個 Akka 版本。

清單 1. 簡單的 Scala hello
import akka.actor._
import akka.util._

/** Simple hello from an actor in Scala. */
object Hello1 extends App {

  val system = ActorSystem("actor-demo-scala")
  val hello = system.actorOf(Props[Hello])
  hello ! "Bob"
  Thread sleep 1000
  system shutdown

  class Hello extends Actor {
    def receive = {
      case name: String => println(s"Hello $name")
    }
  }
}

清單 1 中的程式碼分為兩個單獨的程式碼段,它們都包含在 Hello1 應用程式專案中。第一個程式碼段是 Akka 應用程式基礎架構,它:

  1. 建立一個 actor 系統(ActorSystem(...) 行)。
  2. 在系統內建立一個 actor(system.actorOf(...) 行,它為所建立的 actor 返回一個 actor 引用)。
  3. 使用 actor 引用向 actor 傳送訊息(hello !"Bob" 行)。
  4. 等待一秒鐘,然後關閉 actor 系統(system shutdown 行)。

system.actorOf(Props[Hello]) 呼叫是建立 actor 例項的推薦方式,它使用了專門用於 Hello actor 型別的配置屬性。對於這個簡單的 actor(扮演一個小角色,只有一句臺詞),沒有配置資訊,所以 Props 物件沒有引數。如果想在您的 actor 上設定某種配置,可專門為該 actor 定義一個其中包含了所有必要資訊的 Props 類。(後面的示例會展示如何操作。)

hello !"Bob" 語句將一條訊息(在本例中為字串 Bob)傳送給已建立的 actor。! 運算子是 Akka 中表示將一條訊息傳送到 actor 的便捷方式,採用了觸發即忘的模式。如果不喜歡這種特殊的運算子風格,可使用 tell() 方法實現相同的功能。

第二段程式碼是 Hello actor 定義,以 class Hello extends Actor 開頭。這個特定的 actor 定義非常簡單。它定義必需的(對於所有 actor)區域性函式 receive,該函式實現了傳入訊息的處理方式。(receive 是一個區域性函式,因為僅為一些輸入定義了它 — 在本例中,僅為 String 訊息輸入定義了該函式。)為這個 actor 所實現的處理方法是,只要收到一條 String 訊息,就使用該訊息值列印一條問候語。

Java 中的 Hello

清單 2 給出了清單 1 中的 Akka Hello 在普通 Java 中的表示。

清單 2. Java 中的 Hello
import akka.actor.*;

public class Hello1 {

    public static void main(String[] args) {
        ActorSystem system = ActorSystem.create("actor-demo-java");
        ActorRef hello = system.actorOf(Props.create(Hello.class));
        hello.tell("Bob", ActorRef.noSender());
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) { /* ignore */ }
        system.shutdown();
    }

    private static class Hello extends UntypedActor {

        public void onReceive(Object message) throws Exception {
            if (message instanceof String) {
                System.out.println("Hello " + message);
            }
        }
    }
}

清單 3 顯示了來自包含 lambda 的 Java 8 的 actor 定義,以及 lambda 支援的 ReceiveBuilder 類所需要的匯入。清單 3 或許更加緊湊,但與清單 2 大同小異。

清單 3. Java 8 的 Akka Hello 版本
import akka.japi.pf.ReceiveBuilder;
...
    private static class Hello extends AbstractActor {

        public Hello() {
            receive(ReceiveBuilder.
                match(String.class, s -> { System.out.println("Hello " + s); }).
                build());
        }
    }

與清單 2 相比,清單 3 中的 Java 8 程式碼使用了一個不同的基類 —AbstractActor 代替 UntypedActor— 而且還使用了一種不同的方式來定義訊息處理方案。ReceiveBuilder 類允許您使用 lambda 表示式來定義訊息的處理方式,並採用了類似 Scala 的匹配語法。如果您主要在 Scala 中進行開發工作,此技術可能有助於讓 Java Akka 程式碼看起來更簡潔,但使用 Java 8 特定版本的好處似乎有些微不足道。

為什麼還要等待?

在主應用程式程式碼中,將訊息傳送到 actor 之後,會有一次 Thread sleep 1000 形式的等待,然後才會關閉系統。您可能想知道為什麼需要等待。畢竟,訊息很容易處理;難道訊息沒有立即傳到 actor,在 hello !"Bob" 語句完成時還在處理當中?

這個問題的答案很簡單:“不是”。Akka actor 是非同步執行的,所以即使目標 actor 與傳送方 actor 位於相同的 JVM 中,目標 actor 也絕不會立即開始執行。相反,處理該訊息的執行緒會將訊息新增到目標 actor 的郵箱中。將訊息新增到郵箱中會觸發一個執行緒,以便從郵箱獲取該訊息並呼叫 actor 的 receive 方法來處理。但從郵箱獲取訊息的執行緒通常不同於將訊息新增到郵箱的執行緒。

訊息傳送時間和保證

“為什麼還要等待?” 這一問題的簡短答案的背後是一種更深入的原理。Akka 支援 actor 遠端通訊且具有位置透明性,意味著您的程式碼沒有任何直接的方式來了解一個特定的 actor 是位於同一 JVM 中,還是在系統外的雲中某處執行。但這兩種情況在實際操作中顯然具有完全不同的特徵。

“Akka 無法保證訊息將被傳送到目的地。這種無保證傳送背後的哲學原理是 Akka 的核心原理之一。 ”

一個差別與訊息丟失有關。Akka 無法保證訊息將被傳送到目的地,熟悉訊息傳遞系統(用於連線應用程式)的開發人員可能對此很吃驚。這種無保證傳送背後的哲學原理是 Akka 的核心原理之一:針對失敗而設計。作為一種有意為之的過度簡化,可以認為傳送保證為訊息傳輸系統新增了很高的複雜性,而且這些更復雜的系統有時無法按預期執行,而應用程式程式碼還必須涉及恢復操作。這種原理在應用程式程式碼始終自行處理傳送失敗情況時很有意義,能夠讓訊息傳送系統保持簡單。

Akka 可以 保證訊息最多傳送一次,而且絕不會無序地收到從一個 actor 例項傳送到另一個 actor 例項的訊息。但後者僅適用於特定的 actor 對,二者沒有聯絡。如果 actor A 將訊息傳送給 actor B,這些訊息絕不會被打亂順序。如果 actor A 將訊息傳送給 actor C,情況也是如此。但是,如果 actor B 也將訊息傳送給 actor C(例如將來自 A 的訊息轉發給 C),B 的訊息相對於來自 A 的訊息而言可能是亂序的。

在 清單 1 的程式碼中,訊息丟失的概率非常低,因為程式碼在單個 JVM 中執行,不會生成過多的訊息負載。(過多的訊息負載可能導致訊息丟失。如果 Akka 沒有空間來儲存訊息,例如它沒有備用方案,那麼只能丟棄訊息。)但清單 1 程式碼的結構仍未對訊息傳送事件做出任何假設,而且允許 actor 系統執行非同步操作。

Actor 和狀態

Akka 的 actor 模型很靈活,支援所有型別的 actor。可以使用沒有狀態資訊的 actor(就像 Hello1 示例中一樣),但這些 actor 可能等效於方法呼叫。新增狀態資訊可實現更為靈活的 actor 函式。

清單 1 提供了一個完整的(但很普通)actor 系統示例 — 但擁有一個始終執行同一工作的 actor。每位演員都討厭反覆重複同一句話,所以清單 4 向 actor 新增了一些狀態資訊,使工作變得更有趣。

清單 4. Polyglot Scala hello
object Hello2 extends App {

  case class Greeting(greet: String)
  case class Greet(name: String)

  val system = ActorSystem("actor-demo-scala")
  val hello = system.actorOf(Props[Hello], "hello")
  hello ! Greeting("Hello")
  hello ! Greet("Bob")
  hello ! Greet("Alice")
  hello ! Greeting("Hola")
  hello ! Greet("Alice")
  hello ! Greet("Bob")
  Thread sleep 1000
  system shutdown

  class Hello extends Actor {
    var greeting = ""
    def receive = {
      case Greeting(greet) => greeting = greet
      case Greet(name) => println(s"$greeting $name")
    }
  }
}

清單 4 中的 actor 知道如何處理兩種不同型別的訊息,這些訊息在清單的開頭附近定義:Greeting 訊息和 Greet 訊息,每條訊息都包裝了一個字串值。修改後的 Hello actor 收到 Greeting 訊息時,會將所包裝的字串儲存為 greeting 值。收到 Greet 訊息時,則將已儲存的 greeting 值與 Greet 字串組合起來,形成最終的訊息。下面在執行此應用程式時,我們可以看到在控制檯中列印出的訊息(但訊息不一定是按此順序出現的,因為 actor 執行順序是不確定的):

Hello Bob
Hello Alice
Hola Alice
Hola Bob

清單 4 中並沒有太多的新程式碼,所以我沒有提供其 Java 版本。您可在程式碼下載內容中找到它們(參見 參考資料),名為com.sosnoski.concur.article5java.Hello2 和 com.sosnoski.concur.article5java8.Hello2

屬性和互動

真正的 actor 系統會使用多個 actor 來完成工作,它們彼此傳送訊息來進行互動。並且常常需要為這些 actor 提供配置資訊,以準備履行其具體的職責。清單 5 基於 Hello 示例中使用的技術,展示了簡化版的 actor 配置和互動。

清單 5. Actor 屬性和互動
object Hello3 extends App {

  import Greeter._
  val system = ActorSystem("actor-demo-scala")
  val bob = system.actorOf(props("Bob", "Howya doing"))
  val alice = system.actorOf(props("Alice", "Happy to meet you"))
  bob ! Greet(alice)
  alice ! Greet(bob)
  Thread sleep 1000
  system shutdown

  object Greeter {
    case class Greet(peer: ActorRef)
    case object AskName
    case class TellName(name: String)
    def props(name: String, greeting: String) = Props(new Greeter(name, greeting))
  }

  class Greeter(myName: String, greeting: String) extends Actor {
    import Greeter._
    def receive = {
      case Greet(peer) => peer ! AskName
      case AskName => sender ! TellName(myName)
      case TellName(name) => println(s"$greeting, $name")
    }
  }
}

清單 5 在領導角色中包含了一個新的 actor,即 Greeter actor。Greeter 在 Hello2 示例的基礎上更進了一步,包含:

  • 所傳遞的屬性,目的是配置 Greeter 例項
  • 定義了配置屬性和訊息的 Scala 配套物件(如果您有 Java 工作背景,可將這個配套物件視為與 actor 類同名的靜態 helper 類)
  • 在 Greeter actor 的例項間傳送的訊息

此程式碼生成的輸出很簡單:

Howya doing, Alice
Happy to meet you, Bob

如果嘗試執行該程式碼幾次,可能會看到這些行的順序是相反的。這種排序是 Akka actor 系統動態本質的另一個例子,其中處理各個訊息時的順序是不確定的(但包含我在 “訊息傳送時間和保證” 中討論的幾個重要例外)。

Java 中的 Greeter

清單 6 顯示了清單 5 中的 Akka Greeter 程式碼的普通 Java 版本。

清單 6. Java 中的 Greeter
public class Hello3 {

    public static void main(String[] args) {
        ActorSystem system = ActorSystem.create("actor-demo-java");
        ActorRef bob = system.actorOf(Greeter.props("Bob", "Howya doing"));
        ActorRef alice = system.actorOf(Greeter.props("Alice", "Happy to meet you"));
        bob.tell(new Greet(alice), ActorRef.noSender());
        alice.tell(new Greet(bob), ActorRef.noSender());
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) { /* ignore */ }
        system.shutdown();
    }

    // messages
    private static class Greet {
        public final ActorRef target;

        public Greet(ActorRef actor) {
            target = actor;
        }
    }

    private static Object AskName = new Object();

    private static class TellName {
        public final String name;

        public TellName(String name) {
            this.name = name;
        }
    }

    // actor implementation
    private static class Greeter extends UntypedActor {
        private final String myName;
        private final String greeting;

        Greeter(String name, String greeting) {
            myName = name;
            this.greeting = greeting;
        }

        public static Props props(String name, String greeting) {
            return Props.create(Greeter.class, name, greeting);
          }

        public void onReceive(Object message) throws Exception {
            if (message instanceof Greet) {
                ((Greet)message).target.tell(AskName, self());
            } else if (message == AskName) {
                sender().tell(new TellName(myName), self());
            } else if (message instanceof TellName) {
                System.out.println(greeting + ", " + ((TellName)message).name);
            }
        }
    }
}

清單 7 顯示了包含 lambda 的 Java 8 版本。同樣,此版本在訊息處理的實現方面要更為緊湊,但其他方面都是相同的。

清單 7. Java 8 版本
import akka.japi.pf.ReceiveBuilder;
...
    private static class Greeter extends AbstractActor {
        private final String myName;
        private final String greeting;

        Greeter(String name, String greeting) {
            myName = name;
            this.greeting = greeting;
            receive(ReceiveBuilder.
                match(Greet.class, g -> { g.target.tell(AskName, self()); }).
                matchEquals(AskName, a -> { sender().tell(new TellName(myName), self()); }).
                match(TellName.class, t -> { System.out.println(greeting + ", " + t.name); }).
                build());
        }

        public static Props props(String name, String greeting) {
            return Props.create(Greeter.class, name, greeting);
          }
    }

傳遞屬性

Akka 使用 Props 物件將各種配置屬性傳遞給 actor。每個 Props 例項包裝 actor 類所需的建構函式引數的一個副本,以及對該類的引用。可通過兩種方式將此資訊傳遞給 Props 建構函式。清單 5 中的示例將 actor 的建構函式作為一個名稱傳遞 (pass-by-name) 引數傳遞給 Props 建構函式。注意,此方式不會直接呼叫建構函式並傳遞結果;它傳遞建構函式呼叫(如果您有 Java 工作背景,可能覺得這很陌生)。

將 actor 配置傳遞給 Props 建構函式的另一種方法是,提供 actor 的類作為第一個引數,將 actor 的建構函式引數作為剩餘的引數。對於 清單 5 中的示例,這種呼叫形式為 Props(classOf[Greeter], name, greeting)

無論使用哪種形式的 Props 建構函式,傳遞給新 actor 的值都需要可序列化,以便在必要時通過網路將 Props 傳送到可執行該 actor 例項的任何地方。對於名稱傳遞建構函式呼叫的情況,就像 清單 5 中的用法,需要將呼叫傳送出 JVM 時,會序列化呼叫的閉包

在 Scala 程式碼中建立 Props 物件的 Akka 建議做法是:在一個配套物件中定義工廠方法,就像 清單 5 中所做的那樣。對 Props 使用名稱傳遞建構函式呼叫方法時,此技術可阻止任何問題意外地關閉對 actor 物件的 this 引用。配套物件也是定義 actor 將接收的各種訊息的不錯地方,這樣,所有關聯的資訊都位於同一位置。對於 Java actor,也可在 actor 類中使用靜態建構函式方法,如 清單 6 中所用的方法。

傳送訊息的 Actor

清單 5 中的每個 Greeter actor 都配置了一個名稱和一句問候語,但將問候語告知另一個 actor 時,首先要找到另一個 actor 的名稱。Greeteractor 通過向另一個 actor 傳送一條單獨的訊息來完成此任務:AskName 訊息。AskName 訊息本身不含任何資訊,但收到它的 Greeter 例項知道應使用一個包含 TellName 傳送方名稱的 TellName 訊息作為響應。當第一個 Greeter 收到所返回的 TellName 訊息時,它列印出自己的問候語。

傳送給 actor 的每個訊息都包含由 Akka 提供的一些附加資訊,最特別的是訊息傳送方的 ActorRef。您可在訊息處理過程中的任何時刻,通過呼叫在 actor 基類上定義的 sender() 方法來訪問這些傳送方的資訊。Greeter actor 在處理 AskName 訊息的過程中會使用傳送方引用,以便將 TellName 響應傳送給正確的 actor。

Akka 允許您代表另一個 actor 傳送訊息(一種良性的身份盜竊形式),以便收到該訊息的 actor 將另一個 actor 視為傳送方。這是在 actor 系統中經常使用的一個有用特性,尤其是對於請求-響應型別的訊息交換,因為此時您希望將響應傳送到不同於發出請求的 actor 的某個地方。actor 外部的應用程式程式碼所發出的訊息,預設將使用名為 deadletter actor 的特殊 Akka 作為傳送方。任何時候無法將訊息傳送給 actor 時,也可使用 deadletter actor,這為使用者提供了一種便捷的方式,在 actor 系統中通過開啟合適的日誌(我將在下一期中介紹)來跟蹤無法傳送的訊息。

設定 actor 的型別

您可能注意到了,示例的訊息序列中沒有任何型別的資訊來明確表明訊息的目標是 Greeter 例項。Akka actor 及其交換的訊息一般都屬於這種情況。甚至用於表示訊息目標 actor 的 ActorRef 也是無型別的。

編寫無型別的 actor 系統有著實際的優勢。您可以 定義 actor 型別(比如通過它們可處理的一組訊息),但這麼做有誤導性。在 Akka 中,actor 可以改變它們的行為(下一期會更詳細地介紹此內容),所以不同的訊息集可能適合不同的 actor 狀態。型別也可能妨礙我們合理地簡化 actor 模型,因為系統將所有 actor 視為至少擁有處理任何訊息的潛力。

但是 Akka 仍然支援型別化 actor,以防您確實想要使用這種方法。這種支援在連線 actor 和非 actor 節點時最有用。您可定義一個介面,非 actor 節點使用該介面與 actor 進行通訊,使 actor 看起來更像是正常的程式元件。對於大部分操作,這樣做所帶來的麻煩太多,可能不值得去做,但考慮到從 actor 系統外部直接將訊息傳送給 actor 的簡單性(從目前為止的任何示例應用程式中可以看到,非 actor 程式碼可以傳送訊息),有這個選項也很不錯。

訊息和可變性

Akka 希望您肯定不會意外地在 actor 之間共享可變的資料。如果共享可變的資料,結果會非常糟 — 比不上在對抗幽靈時穿過自己的質子束(如果您不太熟悉,參見電影做鬼敢死隊),但仍然很糟。共享可變資料的問題在於,actor 在單獨的執行緒中執行。如果在 actor 之間共享可變資料,則無法在執行 actor 的執行緒之間進行協調,所以各個執行緒不會看到其他執行緒正在做什麼,並且可能通過多種不同的方式對彼此造成破壞。如果正在執行分散式系統,問題會更嚴重,每個 actor 都將擁有自己的可變資料副本。

所以訊息必須是不可變的,而且不僅僅是在表面層面上。如果訊息資料中包含任何物件,這些物件必須也是不可變的,依此類推,一直到訊息所引用的所有物件。Akka 目前不能強制實施此要求,但 Akka 開發人員希望在將來的某個時刻能強制實施這些限制。如果您希望自己程式碼在未來的 Akka 版本中仍可使用,那麼現在必須留意這一要求。

詢問與告訴

清單 5 中的程式碼使用標準 tell 操作來傳送訊息。在 Akka 中,也可使用 ask 訊息模式作為一種輔助性操作。ask 操作(由 ? 運算子或使用ask 函式表示)傳送一條包含 Future 的訊息作為響應。在清單 8 中,我們重建了清單 5 中的程式碼,使用 ask 來代替 tell

清單 8. 使用 ask
import scala.concurrent.duration._
import akka.actor._
import akka.util._
import akka.pattern.ask

object Hello4 extends App {

  import Greeter._
  val system = ActorSystem("actor-demo-scala")
  val bob = system.actorOf(props("Bob", "Howya doing"))
  val alice = system.actorOf(props("Alice", "Happy to meet you"))
  bob ! Greet(alice)
  alice ! Greet(bob)
  Thread sleep 1000
  system shutdown

  object Greeter {
    case class Greet(peer: ActorRef)
    case object AskName
    def props(name: String, greeting: String) = Props(new Greeter(name, greeting))
  }

  class Greeter(myName: String, greeting: String) extends Actor {
    import Greeter._
    import system.dispatcher
    implicit val timeout = Timeout(5 seconds)
    def receive = {
      case Greet(peer) => {
        val futureName = peer ? AskName
        futureName.foreach { name => println(s"$greeting, $name") }
      }
      case AskName => sender ! myName
    }
  }
}

在清單 8 的程式碼中,TellName 訊息已被替換為 askask 操作返回的 future 的型別為 Future[Any],因為編譯器對要返回的結果一無所知。當 future 完成時,foreach 使用 import system.dispatcher 語句所定義的隱式排程器來執行 println。如果 future 未完成且在允許的超時(另一個隱式值,在本例中定義為 5 秒)內提供了響應訊息,它會完成並丟擲超時異常。

在幕後,ask 模式建立一個特殊的一次性 actor 在訊息交換中充當中介。該中介會收到一個 Promise 和要傳送的訊息,以及目標 actor 引用。它傳送訊息,然後等待期望的響應訊息。收到響應後,它會履行承諾並完成最初的 actor 所使用的 future。

使用 ask 方法有一些限制。具體來講,要避免公開 actor 狀態(可能導致執行緒問題),必須確保您未在 future 完成時所執行的程式碼中使用來自該 actor 的任何可變狀態。在實際情況中,為在 actor 之間傳送的訊息使用 tell 模式通常要更容易。ask 模式更有用的一種情況是,應用程式程式碼需要從 actor(無論是否具有型別)獲取響應時(比如啟動 actor 系統和建立初始 actor 的主程式)。

小角色

“在對您明確處理非同步操作有所幫助時,應毫不猶豫地向設計中引入新的 actor。”

ask 模式所建立的一次性 actor 是在使用 Akka 時要記住的一種出色設計原則。通常您希望構造您的 actor 系統,以便中間處理步驟是由為這種特定的用途而設計的特殊 actor 所執行的。一個常見的例子是,需要在進入下一處理階段之前合併不同的非同步結果。如果為不同的結果使用訊息,您可讓一個 actor 來收集各個結果,直到所有結果都準備好,然後觸發下一階段的操作。這基本上就是 ask 模式所用的一般的一次性 actor。

Akka actor 是輕型的(每個 actor 例項大約 300 到 400 位元組,無論 actor 類使用哪種儲存都是如此),所以您可安全地設計程式結構,在適當的時候使用多個 actor。使用專用的 actor 有助於保持程式碼簡單且易於理解,這也是編寫併發程式與編寫順序程式相比的一個優勢。在對您明確處理非同步操作有所幫助時,應毫不猶豫地向設計中引入新的 actor。

補充幾句

Akka 是一個強大的系統,但 Akka 和 actor 模型通常都需要一種與直觀的過程程式碼不同的程式設計風格。對於過程程式碼,程式結構中的所有呼叫都是確定的,並且您可檢視程式的整個呼叫樹。在 actor 模型中,訊息是被樂觀地觸發的,無法保證它們將會送達,而且常常很難確定事件發生的順序。actor 模型的好處是,這是一種構建高併發性和可伸縮性應用程式的輕鬆方式,我在後面的幾期中會再次介紹此主題。

希望本文能夠讓您足夠清楚地瞭解 Akka,激起您進一步探索該內容的慾望。下一次我將更深入地介紹 actor 系統和 actor 互動,包括如何輕鬆地跟蹤系統中各 actor 之間的互動。

相關文章