ForkJoinPool vs ThreadPoolExecutor

12月27日のJJUGナイトセミナーLT大会に参加しました(LT資料)。

会場片付けの際、マルチスレッド処理に関して発表された @hiroga_cc さんと、ForkJoinPoolThreadPoolExecutorの使い分けについて、少しだけおしゃべりしました。その場では頭がこんがらがってしまったので、改めて整理します。

要旨

ForkJoinPoolは、次のようなジョブが効率的に実行できるように設計されたExecutorです。

  • 各タスクがIOをともなわない、CPUヘビーな処理であること。
  • 各タスクの処理が、新しいタスクを産み出すような、再帰的タスク構成であること。

上記の条件を満たさないジョブに対して、ForkJoinPoolが使えないというわけではありません。ただし、タスク発生状況に応じてに応じてスレッドの生成・破棄を制御したい場合などは、ThreadPoolExecutorの方が適しています。

構成と動作

ForkJoinPool, ThreadPoolExecutorとも、下記の点では共通しています。

  • マルチスレッドで、並列にタスクを処理するための仕組みである。
  • ワーカースレッド群と、タスクキューの組み合わせで構成される。

異なる点はタスクキューの構成と取り回しです。ForkJoinPoolでは、ワーカースレッドが可能な限りブロックせずに動作し続けられるように、各ワーカースレッドにひもづいてタスクキューが用意されます。具体的な構成・動作の差異は次のとおりです。

  • ThreadPoolExecutor
    • 全ワーカースレッドがひとつのタスクキューを共有する
    • タスクの投入は共有のタスクキューに対して行う (→ 競合しやすい)
    • ワーカースレッドは共有のタスクキューからタスクを取得する (→ 競合しやすい)
  • ForkJoinPool
    • 各ワーカースレッドが自分のタスクキューを持っている
    • ワーカースレッド内で発生したタスクは、自分のタスクキューに投入される (→ 競合しにくい)
    • ワーカースレッドは、まず自分のタスクキューからタスクを取得する (→ 競合しにくい)
    • 自分のタスクキューにタスクがない場合のみ、他のスレッドのキューからタスクを盗む (work-stealing) (→ 競合しやすい)

ForkJoinPoolでは、ワーカースレッド内で発生したタスクをさばききるまで、自分のスレッドにひもづいたタスクキューだけにタスクの投入、取得を行います。このため、スレッド間競合にともなうブロックなしに計算が続けられます。

IO

タスクの処理がIOをともなう場合、上述したようなForkJoinPoolの利点は消し飛びます。IOは各スレッドが共有するリソースであるため、複数のスレッドが同時にIOを行うと、そこで競合が発生するからです。またIOは、タスクキューの取り回しに比べてはるかに時間のかかる処理であるため、上述したForkJoinPoolのメリットはかき消されます。

上述のとおり、このような場合にForkJoinPoolが使えないわけではありませんが、ThreadPoolExecutorの提供する機能の方が望ましいことが多そうです。

IOによってスレッドがブロックすること自体を避けたい場合は、NIONettyによるノンブロッキングIOを組み合わせることも選択肢となります。