Akkaで始める並行処理(4) - Propsの使用方法とAskパターン
今回はアクターの生成で用いるProps
の詳細と、Askパターンについて解説する。
なお、今回からサンプルコードは主に Scala を用いる。
コンストラクタ引数を持たないアクターの生成
Props
はアクター生成時に用いるオブジェクトで、アクターの型やアクター生成時のパラメータを与える役割を持つ。
今までに登場したアクターの生成コードは次のようなものであった。
val myActor:ActorRef = system.actorOf(Props[MyActor], "myActor")
actorOf
の第一引数に、Props
オブジェクトを MyActor
型を指定して生成している。
第二引数はこのアクターに付ける名前を指している。この引数は省略できるが、次回に述べるアクターの階層で重要な役割を持つので、省略しないことが望ましい。
戻り値が ActorRef
になることも注意が必要だ。 Akka はエラーハンドリングや分散機能を隠蔽して提供するため、ActorRef
というアクターの参照を指すオブジェクトにアクターのインスタンスをラップする。
そのため、アクターを直接生成するのではなく、actorOf
のようなファクトリメソッドや、ファクトリメソッドへのパラメータを指すProps
を提供している。
上のサンプルはMyActor
がコンストラクタ引数を持たない場合の生成方法である。
コンストラクタ引数を持つアクターの生成
次に、アクターにコンストラクタ引数が必要となる場合のProps
の扱いを解説する。
次に示すアクターは、Int
型のコンストラクタ引数を一つ受け取り、
メッセージを受信したらその引数の値の数だけ文字列を繰り返した値を返すものだ。
import akka.actor.{Actor, ActorLogging} import ArgsActor.Input // コンストラクタ引数有りのアクター。 // 注※ Scala ではクラス宣言のパラメータがコンストラクタ引数とフィールドになる class ArgsActor(val repeat:Int) extends Actor with ActorLogging { def receive = { case Input(x) => { log.info(s"revive:${x}") // 文字列をフィールドrepeatの分繰り返したものを返信 sender() ! x * repeat } } } // コンパニオンオブジェクト object ArgsActor { // このアクターで使用するメッセージクラスの定義 case class Input(x:String) }
Scala では、クラス宣言にパラメータを加えることで引数有りのコンストラクタを定義する。
上記の場合、repeat
がコンストラクタ引数とフィールドになる。
また、ArgsActor
と同名の object(コンパニオンオブジェクトと呼ぶ)を用いて、そこにメッセージとして扱うクラス(ここではInput型
)を定義してある。
このようにコンパニオンオブジェクトで、そのアクターが使用するメッセージ型を定義しておくと、メッセージの見通しが良くなる。
上記のアクターにパラメータを与えて生成するには2つの方法がある。
方法1:classOf
の利用
Props
には、クラスとその引数を可変長引数で受け取るメソッドがある。
Scala でクラスを取得するにはclassOf
を使用するため、次のようなコードになる。
val argsActor = system.actorOf(Props(classOf[ArgsActor], 34), "argsActor")
この方法は容易ではあるが、コンストラクタ引数の数や型のチェックを実行時にリフレクションで行うため、ミスに気づくのが遅れてしまう。 そのため、次の方法のほうが安全だ。
方法2:ファクトリメソッドとコンストラクタの利用
Props
にはアクターインスタンスを受け取るメソッドもある。
ただし、公式ドキュメントに書かれている通り、通常のアクターの生成で使用するとメモリリークの危険性があるため、使用箇所が限定される。
安全にコンストラクタを実行できるイディオムとして、次のようなコンパニオンオブジェクトでのファクトリメソッドがある。
// コンパニオンオブジェクト object ArgsActor { // このアクターで使用するメッセージクラスの定義 case class Input(x:String) // パラメータ付きのアクターを生成するファクトリ def props(repeat:Int = 2):Props = Props(new ArgsActor(repeat)) }
この方法だと、メモリリークもなくかつパラメータの型や数もコンパイル時にチェックできる。
ファクトリメソッドとアクターの使用方法のサンプルは次の通りだ。
import akka.actor.{ActorSystem} import akka.pattern.ask import com.example.propsandpath.ArgsActor._ import com.example.propsandpath.ArgsActor import scala.concurrent.Await import scala.concurrent.duration._ object Main extends App { val system = ActorSystem("MyActorSystem") // ? を使うためのスレッドプールとタイムアウトを暗黙的に宣言 implicit val dispatcher = system.dispatcher implicit val timeout:akka.util.Timeout = 3 seconds // ファクトリによるパラメータ付きの生成 val myActor = system.actorOf(ArgsActor.props(3), "argsActor") // ?(askパターン)によってアクターにメッセージを渡し、返ってくるまで待機。 (myActor ? Input("hoge")).mapTo[String].foreach(x => println(x)) // 3秒後に終了 Await.ready(system.terminate(), 3 seconds) }
ArgsActor.props
でファクトリメソッド経由でProps
を取得し、さらにactorOf
でアクター参照を取得している。
その後は、Input
メッセージを送信し、?
という応答が返ってくるまで待機するメソッドを使用して、その結果をコンソールに出している。
この場合"hogehogehoge"
のような文字列がコンソールに出力される。
以上、パラメータをアクターに渡すための方法について解説した。
実際のプログラムではアクターの生成方法を統一した方がプログラムの見通しは良くなる。 そのため、パラメータの有無に関わらず、アクターごとにコンパニオンオブジェクトを用意して、ファクトリメソッドを用意したほうがよいだろう。
Ask パターン
上記のサンプルコードに登場した、?
を使ってアクターの非同期な応答メッセージを待つ手段は、Ask パターンと呼ばれる。
今回のような、アクターシステムとその外部とのやり取りをする場所で使用する。
使用するためには、 ?
が定義してある、akka.pattern.ask
のインポートと、ブロッキングを行うスレッドプールとブロッキングを行う期間を指定しなければならない。
上記サンプルコードでは、implicit
宣言をしたdispatcher
とtimeout
が相当する。
Ask パターンはスレッドプールをブロックするため、子アクターからの応答を待つなど、アクターの内部で多用すると、アプリケーション全体のパフォーマンスに影響を与えてしまう。
そのため、内部で戻り値が必要となるケースでは、第2回記事のサンプルで示した、receive
メソッドで戻り値に相当する型をパターンマッチで取得する方が望ましい。
まとめ
アクターとそのコンパニオンオブジェクトを用いて、ファクトリメソッドとメッセージをまとめるという考えたを示した。 アクター参照という仕組みを用いているため、Akka のアクターの生成は若干クセがあるといえる。
また、アクターの応答を待つ手段として、Ask パターンを紹介した。 最終的な結果を得る場合や、ファイルやDB連携などを行う場合に有用なパターンである。
次回は、アクターの階層について解説する。
参考資料
[前多 賢太郎]