読者です 読者をやめる 読者になる 読者になる

エンタープライズギークス (Enterprise Geeks)

企業システムの企画・開発に携わる技術者集団のブログです。開発言語やフレームワークなどアプリケーション開発に関する各種情報を発信しています。ウルシステムズのエンジニア有志が運営しています。

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宣言をしたdispatchertimeoutが相当する。

Ask パターンはスレッドプールをブロックするため、子アクターからの応答を待つなど、アクターの内部で多用すると、アプリケーション全体のパフォーマンスに影響を与えてしまう。 そのため、内部で戻り値が必要となるケースでは、第2回記事のサンプルで示した、receiveメソッドで戻り値に相当する型をパターンマッチで取得する方が望ましい。

まとめ

アクターとそのコンパニオンオブジェクトを用いて、ファクトリメソッドとメッセージをまとめるという考えたを示した。 アクター参照という仕組みを用いているため、Akka のアクターの生成は若干クセがあるといえる。

また、アクターの応答を待つ手段として、Ask パターンを紹介した。 最終的な結果を得る場合や、ファイルやDB連携などを行う場合に有用なパターンである。

次回は、アクターの階層について解説する。

参考資料

[前多 賢太郎]