Akkaで始める並行処理(3) - アクターの基本 (Java編)
前回の記事では、Scala による Akka の基本的なサンプルプログラムを紹介した。
Akka は Scala と Java それぞれに対応した API を提供している。 今回は、前回 Scala で作成したサンプルプログラムの Java 版を紹介する。
サンプルプログラムの全体構成を以下に再掲するが、詳細に関しては前回の記事を参照していただきたい。
今回のサンプルプログラムも、github で公開している。
子アクターの生成
Java 版では、アクターは、抽象クラスのUntypedActor
を継承して定義する。
まず、子アクターの定義のうち、static
メンバーのメッセージオブジェクトの定義について示す。
// UntypedActor を継承 public class SubActor extends UntypedActor { // イミュータブルオブジェクトとしてメッセージクラスを定義 public static class Response { private final int answer; public Response(int answer) { this.answer = answer; } public int getAnswer() { return answer; } } public static class Request { private final int num; public Request(int num) { this.num = num; } public int getNum() { return num; } }
メッセージオブジェクトは不変(イミュータブル) にすることが推奨されている。
その理由は、可変オブジェクトを他のアクターに渡して共有すると、他のアクターから状態の変更が可能になってしまうためだ。
Java で不変オブジェクトを作るには、final
フィールドと、引数を取るコンストラクタを定義すればよい。
続いて、メッセージ受信時の処理を見ていく。
メッセージ受信処理は、onReceive
メソッドをオーバーライドして定義する。
public class SubActor extends UntypedActor { @Override public void onReceive(Object message) throws Exception { // instanceof によるメッセージ判定 if (message instanceof SubActor.Request) { System.out.println("Start sub at " + new Date()); Thread.currentThread().sleep(1000L); int num = ((SubActor.Request)message).getNum(); System.out.println("Finish sub at " + new Date()); // 送信元へ、メッセージ返信。 getSender().tell(new Response(num * num), getSelf()); } else { // 未処理メッセージの処理を明示する unhandled(message); } } }
メッセージの判定
onReceive
は任意の型のメッセージを受け取るため、引数はObject
型となる。
Scala と違って、Javaにはパターンマッチに該当する機能が無いため、ここではinstanceof
後にダウンキャストしてメッセージの型を判定し、アクセッサを呼び出してフィールドを取得している。
子アクターはRequest
メッセージだけを受け付けるため、ここでは受け取ったメッセージがSubActor.Request
型かどうかを判定している。
また、メッセージオブジェクトが想定しているものでなかった場合は、最後の else
節で、unhandled
メソッドを呼び出し、 Akka のデフォルトのメッセージ処理を呼んでいる。
他のアクターへのメッセージ送信
子アクターは呼び出し元の親アクターに、計算結果メッセージを返信する。
そのための処理が、以下のコードである。
getSender().tell(new Response(num * num), getSelf())
これは Scala 版では、次のようになっていた。
sender() ! Response(num * num)
Java では記号の一部はメソッド名に使えないので、!
がtell
になったのはわかるとして、tell
の第2引数のgetSelf()
は Scala 版にはなかったものだ。(Scala版でgetSelf()
が不要な理由は、この記事の最後で解説する。)
getSelf()
は自分自身のアクター参照を取得するメソッドである。
そして、tell
の第2引数はメッセージの送信者を示している。
つまり、Response
メッセージを送信するのは、自分自身(子アクター)であることを、このメッセージの受信者(今回は親アクター)に伝えている。
実は、アクターのメソッド内で、getSender()
で取得できるアクター参照は、tell
メソッドの第2引数に設定されたアクター参照である。
後で紹介する親アクターの定義でも、子アクターにメッセージを送信する際に、tell
メソッドに親アクター自身の参照を与えている。
そのため、上記コードのgetSender()
で親アクターが取得できている。
tell
メソッドの引数の設定により、メッセージ送信者を自由に設定できるようになっている。
親アクターの定義
続いて、親アクターの定義を見ていく。
public class MyActor extends UntypedActor { private int finished = 0; @Override public void onReceive(Object message) throws Exception { // instanceof によるメッセージ判定 if (message instanceof SubActor.Request) { // 子アクターの生成と参照取得 ActorRef subActor = getContext().actorOf(Props.create(SubActor.class)); // メッセージ送信 subActor.tell(message, getSelf()); } else if (message instanceof SubActor.Response) { SubActor.Response res = (SubActor.Response) message; System.out.println("Answer is " + res.getAnswer()); // 子アクターの停止 getSender().tell(PoisonPill.getInstance(), getSelf()); // 状態の更新と状態の条件に応じた、Akka の終了 finished++; if (finished >= 10) { getContext().system().terminate(); } } else { unhandled(message); } } }
状態用に定義したフィールドに対して、synchronized
などの同期化が不要なのは、Scala と同様である。
子アクターの生成は、ActorContext
を取得して actorOf
メソッドにアクターインスタンス生成用のパラメータを Props
で指定することにより行う。
やはり Scala と同様に、コンストラクタで生成を行ってはいけない。この詳細については今後の記事で解説する。
getContext().actorOf(Props.create(SubActor.class))
その他、instanceof
によるメッセージ判定や、unhandled
の呼び出し、tell
の呼び出し方法は、先ほど示した子アクターのコードと同様だ。
アプリケーションの起動
最後に Akka アプリケーションを起動するコードを紹介する。ActorSystem
を作成し、親アクターを生成して、メッセージを送信していく。
public class ApplicationMain { public static void main(String[] args) throws TimeoutException,InterruptedException{ ActorSystem system = ActorSystem.create("MyActorSystem"); ActorRef myActor = system.actorOf(Props.create(MyActor.class), "myActor"); for(int i = 0; i < 10; i++) { myActor.tell(new SubActor.Request(i), null); } Await.ready(system.whenTerminated(), Duration.apply(1, TimeUnit.MINUTES)); } }
ほぼ、Scala 版と同様であるが、 親アクターに対するtell
メソッドの第2引数にはnull
を指定している。
これは、Mainクラスは、Akka アプリケーションの外にあるため、送信者のアクターを指定できないためだ。
Scala版とJava版の違いについて
メッセージオブジェクトの定義
メッセージオブジェクトは、不変オブジェクトとして定義する。
Java版では、final
フィールドと getter、引数有りのコンストラクタが必要だ。
一方 Scala では、case class Request(num:Int)
のように、ケースクラスの定義だけで上記とほぼ同等の定義が可能だ。
メッセージ判定処理の方法
Java版ではinstanceof
による型判定を行ったが、Scala 版ではパターンマッチングによる判定が使える。
unhandledの呼び出し
Java版では、onReceive
メソッドで必ず unhandled
メソッドを呼ぶ必要がある。
ここは間違いを起こしやすいので注意が必要だ。
tell メソッドの送信者の明示
Java 版では、tell
メソッドで、送信者の指定を明示する必要がある。ここも、Scala版と大きく異なる点なので注意が必要だ。
少々余談となるが、Scala版ではなぜ、これが不要になるのかを解説する。
実は、Scala 版の !
メソッドの本当の定義は以下のようになっている。
def !(message: Any)(implicit sender: ActorRef = Actor.noSender): Unit
Scala ではメソッド引数の括弧を複数個定義できる。2個目の括弧には、Java のtell
メソッド同様に、アクター参照を受け取るようになっている。
どうして、Scala 版では第2引数を指定しなくてよいかというと、引数のimplicit
キーワードが関係している。
これは implicit parameter と呼ばれ、引数の値をスコープ内で implicit 宣言された値から自動的に補完する機能である。
Scala では Actor
トレイト内に、以下のようにself
が implicit 宣言されている。
implicit final val self = context.self
このため、!
メソッドにsender
を明示的に指定しない場合は、self
が自動的に渡るようになっている。
また、 Actor.noSender
はメソッドの引数がない場合のデフォルト値で null に相当する。
よって、 mainメソッドなど、implicit
宣言がない場所で呼んだ場合は、この値がデフォルト値で設定されることになる。
アクターへの機能追加の方法
アクターに機能を追加する方法も、言語仕様の違いのため Scala と Java では異なる。
例えば、Scala 版では アクターにログ機能を追加するには、以下のようにActorLoging
トレイトの多重継承を行う。
class SomeActor extends Actor with ActorLogging{ def receive = { case _ => log.info("messeage") } }
一方、Java では、トレイトの多重継承の代わりに、委譲で機能を追加する。
public class SubActor extends UntypedActor { private LoggingAdapter log = Logging.getLogger(getContext().system(), this); @Override public void onReceive(Object message) throws Exception { log.info("message"); } }
まとめ
Java 版の Akka のサンプルプログラムを紹介した。 Scala 版と同じAPIを提供しているため、機能の違いは無いが、JavaとScalaの言語仕様の違いによって記述の仕方はだいぶ異なる。 開発メンバーの言語の習熟度や参考情報の有無などを考慮して、どちらで開発するかを選択するべきだろう。
ちなみに、Akka に関する書籍やWeb上の資料は Scalaの方が多い。 その理由としては、Scalaの方がパターンマッチングなどの機能を使って簡潔にプログラムを書けるからだと思われる。
この連載でも今後は主に Scala でサンプルコードを紹介していく。
次回は、Props と アクター階層について解説を行う。
[前多 賢太郎]