12月27日のJJUGナイトセミナーLT大会に参加しました(LT資料)。
会場片付けの際、マルチスレッド処理に関して発表された @hiroga_cc さんと、ForkJoinPoolとThreadPoolExecutorの使い分けについて、少しだけおしゃべりしました。その場では頭がこんがらがってしまったので、改めて整理します。
要旨
ForkJoinPoolは、次のようなジョブが効率的に実行できるように設計されたExecutorです。
- 各タスクがIOをともなわない、CPUヘビーな処理であること。
- 各タスクの処理が、新しいタスクを産み出すような、再帰的タスク構成であること。
上記の条件を満たさないジョブに対して、ForkJoinPoolが使えないというわけではありません。ただし、タスク発生状況に応じてに応じてスレッドの生成・破棄を制御したい場合などは、ThreadPoolExecutorの方が適しています。
構成と動作
ForkJoinPool, ThreadPoolExecutorとも、下記の点では共通しています。
- マルチスレッドで、並列にタスクを処理するための仕組みである。
- ワーカースレッド群と、タスクキューの組み合わせで構成される。
異なる点はタスクキューの構成と取り回しです。ForkJoinPoolでは、ワーカースレッドが可能な限りブロックせずに動作し続けられるように、各ワーカースレッドにひもづいてタスクキューが用意されます。具体的な構成・動作の差異は次のとおりです。
- ThreadPoolExecutor
- 全ワーカースレッドがひとつのタスクキューを共有する
- タスクの投入は共有のタスクキューに対して行う (→ 競合しやすい)
- ワーカースレッドは共有のタスクキューからタスクを取得する (→ 競合しやすい)
- ForkJoinPool
- 各ワーカースレッドが自分のタスクキューを持っている
- ワーカースレッド内で発生したタスクは、自分のタスクキューに投入される (→ 競合しにくい)
- ワーカースレッドは、まず自分のタスクキューからタスクを取得する (→ 競合しにくい)
- 自分のタスクキューにタスクがない場合のみ、他のスレッドのキューからタスクを盗む (work-stealing) (→ 競合しやすい)
ForkJoinPoolでは、ワーカースレッド内で発生したタスクをさばききるまで、自分のスレッドにひもづいたタスクキューだけにタスクの投入、取得を行います。このため、スレッド間競合にともなうブロックなしに計算が続けられます。
IO
タスクの処理がIOをともなう場合、上述したようなForkJoinPoolの利点は消し飛びます。IOは各スレッドが共有するリソースであるため、複数のスレッドが同時にIOを行うと、そこで競合が発生するからです。またIOは、タスクキューの取り回しに比べてはるかに時間のかかる処理であるため、上述したForkJoinPoolのメリットはかき消されます。
上述のとおり、このような場合にForkJoinPoolが使えないわけではありませんが、ThreadPoolExecutorの提供する機能の方が望ましいことが多そうです。
IOによってスレッドがブロックすること自体を避けたい場合は、NIOやNettyによるノンブロッキングIOを組み合わせることも選択肢となります。