こんなコードで実験。
普通に考えると、Parallel()はJVM内で共通のPOOLを使い、使えない場合はcaller threadを使うようになる。
現に、ExecutorServiceを使う場合、そのような(commonのforkjoinとcaller threadを使う)挙動になる。
なのでparallel().foreachの中で重い処理を書くと全体に影響が出る。(もちろんcaller threadで実行されので処理が止まってしまうわけでは無い)
しかしながら、ForkJoinPool
を使った場合、全てのスレッドがForkJoinPool
で実行されるのである。なぜ共通のPoolが使われないのか?
//ExecutorService forkJoinPool = Executors.newFixedThreadPool(2); |
ThreadPoolExecutorの場合
[3]Wed Oct 18 02:04:18 JST 2017 ForkJoinPool.commonPool-worker-3 |
ForkJoinPoolの場合
[2]Wed Oct 18 02:03:06 JST 2017 ForkJoinPool-1-worker-0 |
WHY..?
なぜだろうと思いソースを追いかけると、ForkJoinTask
で次のコードが。
private int doInvoke() { |
parallel()
で使われるForkJoinPool.commonPool
もクラスはForkJoinWorkerThread
なので、ちょっと特別扱いされてるのかー。
ていうか、こんなん分かるわけないよ。
でも、これって実行した後の話だよね。。
“commonで実行しようと思ったけど、callerがforkjoinだからそっちで実行しよう” みたいな処理じゃないと辻褄が合わない気がする。他の場所にあるのかな?
ForkJoinTask
のこれか。
public final ForkJoinTask<V> fork() { |
- ForkJoinTaskは木構造を作っている(これを実行したらこれを実行するみたいなグラフ)
- ForkJoinスレッドの中でForkJoin(parallel()も同様)を実行すると、この構造に連結されていく
- ForkJoinPool.submit(stream.parallel().forEach) はこの構造を一気に構築する。
- この構造上、途中に遅いヤツが居ると後続の処理は全て引きずられる
- 待機列を一気に構築して、それぞれ処理する。並び替えは不可。というイメージ。
- 時間が経過すると、一番遅いヤツに引きずられて空きWorkerだらけになる。
- (スーパーのレジのイメージで、かつ100人の客が居たらいっきに100人分の列と順番を決めてしまう)
- これに対してExecutorServiceは1つの行列(Queue)にタスクを積んで、空きWorkerができ次第順処理される
- (コンビニのレジの列のイメージ)
この記事 の現象はまさにそれかなと思いました。