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

ましめも

技術系メモ

Future内でThread.sleepはするな

前回の記事(Scala ExecutionContextって何 / Futureはスレッド立ち上げじゃないよ - ましめも) で import scala.concurrent.ExecutionContext.Implicits.global とは何なのか、そもそも ExecutionContext とは ということについて解説した。

おさらい

  • ExecutionContext は スレッドプールを持っていて、そこにタスクを割り当てる機構
  • ExecutionContext.Implicits.global はデフォルトではCPUコア数分のスレッドを持っている

Future内でThread.sleepはご法度

ExecutionContext.Implicits.global を使っている状態でThread.sleepをすると非常に迷惑になることがある。
例えば次のようなコードがあったとして、CPUコア数4のマシンで関数fugaと関数hogeが同時に実行されると一体どういうことになってしまうか?

import scala.concurrent._
import ExecutionContext.Implicits.global
def fuga = {
  // CPUコストのかかる function1 という関数をマルチコアを使って効率よく処理したい
  (1 to 10).map {e =>
    Future {function1(e)}
  }
}
def hoge = {
  // 3秒待ってから処理するのを簡単に書いてみた
  (1 to 3).map {e =>
    Future {Thread.sleep(3000); function2(e)} 
  }
}

ご想像の通り、処理がつっかえてしまう(下図)。

f:id:mashijp:20141207235401p:plain

もちろんCPU使用率が100%であればマシン資源をフルに使っているからいいのだが、緑色のタスクはただ寝てるだけだ!(Thread.sleep しているだけ) 資源を無駄にしてしまっている!
橙色のタスクは律儀にglobalのスレッドが空くのをただただ待っている状態だ…

よって、ExecutionContext.Implicits.global を使う場合に原則 Thread.sleep を行ってはいけない。同様に、ブロッキングが発生する操作を行うべきではない(IOのblockingなど)。ここ、みんなの使うところだから。

Thread.sleepしたい場合やIOのblockingを行う場合どうすりゃいいのか

タイトルは半分ウソで、絶対にThread.sleepしてはいけないわけではない。みんなで共用する(可能性のある) ExecutionContext.Implicits.global でそういうことをすると思わぬパフォーマンスの低下を生む可能性があるわけであって、自分で使う用の ExecutionContext を作ってそれを使えば問題ない。

ExecutionContext のscaladocにもそう書いてある

A custom ExecutionContext may be appropriate to execute code which blocks on IO or performs long-running computations. ExecutionContext.fromExecutorService and ExecutionContext.fromExecutor are good ways to create a custom ExecutionContext.

例えばこんな感じ

import scala.concurrent._
import java.util.concurrent.Executors
val ec = ExecutionContext.fromExecutorService(Executors.newFixedThreadPool(2))
(1 to 30).foreach {_ => Future{Thread.sleep(3000); println(new java.util.Date)}(ec)}
/* 2個ずつ動いている
Sun Dec 07 23:47:55 JST 2014
Sun Dec 07 23:47:55 JST 2014
Sun Dec 07 23:47:58 JST 2014
Sun Dec 07 23:47:58 JST 2014
Sun Dec 07 23:48:01 JST 2014
Sun Dec 07 23:48:01 JST 2014
Sun Dec 07 23:48:04 JST 2014
**/

これで、global側を邪魔しない。

なおPlay frameworkを使っている場合だと、内包してる akka で簡単に ExecutionContext を作ることができたりする ("Many specific thread pools" のところ参照)
https://www.playframework.com/documentation/2.3.x/ThreadPools

ExecutionContext.Implicits.global のご利用は計画的に

ここまで書いて、ExecutionContext とトイレ(個室)はほとんど同じなんじゃと思い始めた。ExecutionContext.Implicits.global は共用トイレと考えると…

  • 個室の数(= Thread数 = 同時に処理できる数)には限りがある。先着順で処理する
  • 空いてるときは100歩譲って寝てようと何してようと問題にならない
  • 待ち行列ができてるにも関わらずトイレ(ExecutionContext)で寝てるのは許せない。本当に用を足してるならしょうがない
  • 待ち行列ができることに何も問題ないと言い切れるなら別に構わない
  • 自分で作った自分専用のトイレ(ExecutionContext)なら自由にしてくれ

うん!大体あってる気がする!それでは快適なトイレライフを!