Actor Extensionはスレッドアンセーフな点に注意


以下の記事の通りActor Extensionを調べたのですが、1点注意点を見つけたので追記です。

Akka ActorのExtensionを少し調べてみた

結論:スレッドアンセーフ

ActorSystemごとに1インスタンス、全Actorから共通して使える、状態も持てる。という訳なので、やはり複数Actorから同時にアクセスされればレースコンディションが発生します。スレッドアンセーフです。

ダメなコード例

前回の例を少し変えて以下のようなコードを実行してみます。

import akka.actor._
import akka.pattern._
import akka.util.Timeout

import scala.concurrent.Await
import scala.concurrent.duration._
import scala.language.postfixOps

object Main {
  def main(args: Array[String]): Unit = {
    implicit val timeout = Timeout(10 seconds)
    val system1 = ActorSystem("my-system")

    val actor1 = system1.actorOf(Props(classOf[MyActor]))
    val actor2 = system1.actorOf(Props(classOf[MyActor]))

    (0 until 10000) foreach { x =>
      actor1 ! MyActor.Protocol.Increment
      actor2 ! MyActor.Protocol.Increment
    }

    Thread.sleep(5000)
    val count = Await.result(actor1 ? MyActor.Protocol.GetCount, Duration.Inf)
    println(s"count = $count")

    system1.terminate()
  }
}

class MyActor extends Actor {
  val counter = CountExtension(context.system)

  def receive = {
    case MyActor.Protocol.GetCount => sender() ! counter.get()

    case MyActor.Protocol.Increment => counter.increment()
  }

}

object MyActor {

  object Protocol {
    object Increment
    object GetCount
  }

}

class CountExtensionImpl extends Extension {
  // private val counter = new AtomicLong(0)
  private var counter = 0L

  //def increment() = counter.incrementAndGet()
  def increment() = {
    counter += 1
    counter
  }

  //def get() = counter.get()
  def get() = counter
}

object CountExtension
  extends ExtensionId[CountExtensionImpl]
  with ExtensionIdProvider {

  override def lookup = CountExtension

  override def createExtension(system: ExtendedActorSystem) = new CountExtensionImpl

  override def get(system: ActorSystem): CountExtensionImpl = super.get(system)
}

Actorを2つ作って、それぞれ10000回メッセージを送っています。これにより合計20000回incrementされて、最後のGetCountにより、20000がprintlnされるように見えます。が、実際はそうなりません。

count = 19975

こんな感じで実行するたびに値が異なり、20000になりません。競合していますね。

対策

今回の例だとAtomicLongを使うとか。そうでない場合は無難にsynchronizedでしょうか。GitHubでコード検索したらsynchronizedをちらほら見かけました。

という訳でActorを使っているとついついレースコンディション周りをおろそかにしがちですが、Extensionを使う場合は十分気をつけましょう。