Effective Akkaの中で、「askを使うよりもなるべくtellを使いなさい」と言われる理由の一つとしてコストが挙げられていたけれども、実際のところどれだけコストが違うのかというところをfutureベースのコード(≒ ask)とtellベースのコードを作って確かめてみた。
futureベースのサンプルコード
import java.lang.management.ManagementFactory
import scala.concurrent.{Await, ExecutionContext, Future}
import scala.concurrent.duration.Duration
import scala.concurrent.forkjoin.ForkJoinPool
import scala.util.Random
object FutureSeqSample extends App {
val pid = ManagementFactory.getRuntimeMXBean.getName.split('@').head
println(s"jmap -histo:live ${pid}")
val forkJoinExecutor = new ForkJoinPool(10)
implicit val ec = ExecutionContext.fromExecutor(forkJoinExecutor)
val futures = for (i <- 1 to 100000) yield {
Future {
Thread.sleep(300000000L)
Random.nextInt()
}
}
val f = Future.sequence(futures)
f onSuccess {
case seq => println(s"sum is ${seq.reduceLeft(_+_)}")
}
Await.result(f, Duration.Inf)
Thread.sleep(30000000L)
}
tellベースのサンプルコード
import java.lang.management.ManagementFactory
import scala.concurrent.Await
import scala.concurrent.duration.{Duration, DurationInt}
import scala.util.{Random, Success}
import akka.actor.{Actor, ActorRef, ActorSystem, Props, Stash, actorRef2Scala}
import akka.pattern.ask
import akka.routing.RoundRobinPool
import akka.util.Timeout
object ActorSeqSample extends App {
val pid = ManagementFactory.getRuntimeMXBean.getName.split('@').head
println(s"jmap -histo:live ${pid}")
implicit val timeout = Timeout(100 seconds)
val system = ActorSystem()
import system.dispatcher
val executor = system.actorOf(Props[Executor])
val result = executor ? Start(100000)
result.mapTo[CalculateResult] onComplete {
case Success(sum) => println(s"sum is ${sum.n}")
case _ => // このサンプルでは失敗しない
}
Await.result(result, Duration.Inf)
Thread.sleep(300000L)
}
case class Start(n: Int)
case class CalculateResult(n: Int)
class Executor extends Actor with Stash {
var sum = 0
var resultCount = 0
var maxCount = 0
val randomIntActors = context.actorOf(new RoundRobinPool(10).props(Props[RandomIntActor]))
def receive: Actor.Receive = {
case Start(n) =>
for (i <- 1 to n) {
randomIntActors ! "do it"
}
sum = 0
resultCount = 0
maxCount = n
context become calculate(sender)
}
def calculate(originalSender: ActorRef): Actor.Receive = {
case CalculateResult(n) =>
println(n)
resultCount += 1
if (resultCount >= maxCount) {
originalSender ! CalculateResult(sum)
context.unbecome()
unstashAll()
}
case _ => stash()
}
}
class RandomIntActor extends Actor {
def receive = {
case msg =>
Thread.sleep(3000000L)
sender ! CalculateResult(Random.nextInt())
}
}
計測結果(jmap)
- future
>jmap -histo:live 10208 | find "00"
1: 200001 3200016 scala.concurrent.impl.Promise$DefaultPromise
2: 100010 2400240 scala.collection.immutable.$colon$colon
3: 100003 2400072 scala.concurrent.impl.CallbackRunnable
4: 100001 2400024 scala.concurrent.impl.ExecutionContextImpl$AdaptedForkJoinTask
5: 100000 2400000 scala.concurrent.Future$$anonfun$flatMap$1
6: 100000 2400000 scala.concurrent.Future$$anonfun$sequence$1$$anonfun$apply$10
7: 100000 2400000 scala.concurrent.impl.Future$PromiseCompletingRunnable
8: 100000 1600000 future.samples.FutureSeqSample$$anonfun$2$$anonfun$apply$1
- tell
>jmap -histo:live 5540 | find "00"
1: 100007 2400168 java.util.concurrent.ConcurrentLinkedQueue$Node
2: 100000 2400000 akka.dispatch.Envelope
サンプルは10万回の演算命令ですが、futureベースでやろうとした場合、その時点で結果を受け取るための様々なオブジェクトが作られてしまうけれども、tellベース場合はmailboxにメッセージを入れるコストくらいしかかかっていない、ということが言えそうです。