akkaにおけるActorの生成時の注意事項

akkaをある程度使いこなしてる人なら、当たり前?かもしれませんが、その細かい仕組みがわかりずらいと思うので説明を書いてみます。(versionによってそれほど変わらないはずですが)これを書いてる時点の最新の akka 2.3.4、Scala 2.11.2 としておきます。


akkaの公式ドキュメントに以下の様な例と説明があります
http://doc.akka.io/docs/akka/2.3.4/scala/actors.html

import akka.actor.Props
 
val props1 = Props[MyActor]
val props2 = Props(new ActorWithArgs("arg")) // careful, see below
val props3 = Props(classOf[ActorWithArgs], "arg")

そこの「careful, see below」を(akkaのドキュメントにも書いてあるが)自分なりに、Scala自体の内部実装を含めて詳しく説明するエントリです。

akkaのActorは、基本的にPropsというものを経由して生成します。(そのままnewする場合もありますが、newしても結局すぐPropsで包む) *1
いくつかの理由(全部きちんと把握してないし、そこは公式ドキュメント読んで)がありますが、akkaのActorは直接参照を保持せずに、このPropsやActorRefを通して扱わないといけません。
さて、その「直接参照を保持してはいけない」がだいぶ面倒なことに気が付きました。基本的に現時点でのScalaとAkkaでは、それをコンパイラに完璧にはチェックさせることが不可能なので気をつけるしかありません。
ただ、基本的なパターンは

Props(classOf[生成したいActorのクラスの型], 引数1, 引数2)

のほうを使うと防げます。それは、以下の様に書けて、それでも基本的には同じなのですが

Props(new 生成したいActorのクラスの型(引数1, 引数2))

このように直接newすると、参照保持してしまってメモリリークなどの問題を引き起こす可能性があります。例えば下記が実際に駄目なパターンです。なぜ駄目か分かりますか?

import akka.actor._
 
case class Foo(a: Int) extends Actor{
  override def receive = ???
}
 
class Bar extends Actor{
  val b = util.Random.nextInt
 
  override def receive = {
    case message =>
      val child = context.actorOf(Props(new Foo(b)))
  }
}

これを -Xprint:jvm という引数付けてコンパイルしてみましょう(途中のASTを表示してくれるコンパイラオプション)。
長いので全部貼り付けません、gistのリンクだけ貼っておきます

https://gist.github.com/xuwei-k/569f16ee8d293e775de8

必要な部分だけ抜き出すと以下です

@SerialVersionUID(0) final <synthetic> class $anonfun$1 extends scala.runtime.AbstractFunction0 with Serializable {
  final def apply(): Foo = new Foo($anonfun$1.this.$outer.$outer().b());
  <synthetic> <paramaccessor> <artifact> private[this] val $outer: <$anon: Function1> = _;
  final <bridge> <artifact> def apply(): Object = $anonfun$1.this.apply();
  def <init>($outer: <$anon: Function1>): <$anon: Function0> = {
    if ($outer.eq(null))
      throw null
    else
      $anonfun$1.this.$outer = $outer;
    $anonfun$1.super.<init>();
    ()
  }

AbstractFunction0というのを継承したclassがあります。これは、どの部分かというと、Props(new Foo(b)) の、Props#applyに引き渡してる引数です。

「Function0なんで作ってないよね?」

と思いますよね?ここでProps#applyのシグネチャを見てみましょう

https://github.com/akka/akka/blob/v2.3.4/akka-actor/src/main/scala/akka/actor/Props.scala#L84

def apply[T <: Actor: ClassTag](creator: ⇒ T): Props =

名前渡し?と言うんでしょうか?この creator: ⇒ T の部分は、JVMのclassファイルに落ちた時は Function0 になります。これがポイントです。まず「Function0になる」というのを知ってないと、理解難しいですね。

そして、この場合のFunction0は元のソースコード中では new Foo(b) で、「外側のclass BarのフィールドBar#b」を参照していますね?このbはvalなので、値変わらないし、そのbの値だけ渡してくれればいいですが、Scalaコンパイラはそこまで勝手に最適化しない*2というか頭良くありません。

(余談としてそのあたりを解決するかもしれないsporeがある http://d.hatena.ne.jp/xuwei/20130622/1371781212 )

つまりnew Foo(b)で、bを後から取得できるようにするために「Barのインスタンス自体を間接的に保持したFunction0」を生成します。「Barのインスタンス」はActorなので、保持してはいけません。

先ほどのASTは

def <init>($outer: <$anon: Function1>): <$anon: Function0> = {

と、Funciton0を生成するために、自身のコンストラクタでFunction1を受け取っていますね?これまた「Function1ってどこからでてきたんだよ」と思いますが、Bar#receive の PartialFunction でしょう(PartialFunctionはFunction1のサブクラスである) *3

ASTだと

@SerialVersionUID(0) final <synthetic> class $anonfun$receive$1 extends scala.runtime.AbstractPartialFunction

というclassがあり、そのフィールドで def $outer(): Bar というメソッドがありますね?

つまり Bar#b を取得するために


Props#applyの引数のためのFunction0は、 Bar#receive の PartialFunctionを保持、そしてその PartialFunctionは Bar のインスタンスを保持


という関係になっていて、結局Props#applyから間接的にBarのインスタンスの参照を渡してしまってることになります。コードはできるだけ単純な例にしたつもりだけど、AST出力して、1つずつ説明すると、それなりに長くなりますね。つかれた・・・


上記のパターンなら

Props(classOf[Foo], b)

とするだけで防げます。しかし、これやると、Actorのコンストラクタの引数の型や数がチェックされず、それらを変更した場合、テストや実行時までミスに気づけないデメリットがあります。
Akkaつらい・・・

また「ならclassOfのほうを必ずつかえば防げるのか」と考えますが、それでも防げないパターンがあると思います。以下に例を作っておいたので、ASTと合わせて見てみてください(詳しい解説はもう疲れたので・・・)

https://gist.github.com/xuwei-k/b5719c601df3f6a8dbb9

import akka.actor._
 
case class Foo(a: Int => Int) extends Actor{
  override def receive = ???
}
 
class Bar extends Actor{
  val b = util.Random.nextInt
 
  override def receive = {
    case message =>
      val child = context.actorOf(Props(classOf[Foo], { c: Int => c + b }))
  }
}


このパターンだと
「Actorの引数にFunction0やFunction1、もしくは無名クラスを受け渡す場合は気をつけよう」
という結論にしかならない気がします。Akkaつらい・・・。
実はだれか解決策知ってるなら教えて下さい。*4まぁそもそも、(Actorの役割や、生成されうる数によっては)それほど、この参照を保持することを気にしなくていいのかもしれません?が、そのあたりがよくわかってません。

*1:あとである意味説明がありますが、"そのままnewする"というより、実は "newするためのFunction0を渡している" ので、"newした後に包む" わけではない

*2:というか、最適化してはいけないパターンも有るはず。この場合が最適化してはいけないパターンなのかどうか、はわからないが

*3:Actorのreceiveの型はPartialFunction[Any, Unit]である https://github.com/akka/akka/blob/v2.3.4/akka-actor/src/main/scala/akka/actor/Actor.scala#L328

*4:この前macro annotationでやったことあるけど、sbtがバグるしIDE対応してないし、全然使えない・・・ https://twitter.com/xuwei_k/status/481303929431420928