ForkJoinPool
#Fork/Join #Java #並行プログラミング #スレッドプール
通常のスレッドプール(例:FixedThreadPool)
普通のスレッドプールは「キューにタスクを積み、ワーカーが順番に処理」します。
code:text
┌───────────────┐
タスク投入 → │ 共有キュー │ ←── ワーカー1
│ ABCD │ ←── ワーカー2
└───────────────┘
利点:実装が簡単で順序が安定。
欠点:一つのスレッドが長時間ブロックすると、他のタスクが詰まる。
ForkJoinPoolとは?
ForkJoinPool は、「タスクを細かく分割(Fork)し、完了後に結果を統合(Join)する」並列処理のための特別なスレッドプール
普通のプールとの違い
各スレッドが独自のタスクキューを持つ
各スレッドが自分専用の「デック(両端キュー)」を持ちます。
code:text
Worker1 ─▶ A1, A2, A3
Worker2 ─▶ B1, B2
Worker3 ─▶ C1, C2, C3, C4
Work-Stealingアルゴリズム
あるスレッドが暇になったら、他のスレッドのキューからタスクを「盗む」ように動きます。
負荷が自動的に分散される
スレッドがアイドルになりにくく、CPU使用率が高い
再帰的並列化に強い
「問題を分割(fork)して再帰的に処理→結果を統合(join)」する処理に最適化されています。
code:scala
def sum(arr: ArrayInt): Int =
if (arr.length <= 1) arr.sum
else {
val (left, right) = arr.splitAt(arr.length / 2)
val leftF = fork(sum(left))
val rightF = fork(sum(right))
join(leftF) + join(rightF)
}
code:text
+-----------------------------------+
| ForkJoinPool |
|-----------------------------------|
| Worker1 ─▶ deque: T1, T2 |
| Worker2 ─▶ deque: T3 |
| Worker3 ─▶ deque: [] |
+-----------------------------------+
│
▼
Worker3 steals T2 from Worker1
各スレッドは自分のキューを「LIFO(後入れ先出し)」で処理し、
他人のキューは「FIFO(先入れ先出し)」で奪う。
→ これでキャッシュ局所性と公平性を両立します。
Cats Effect と ForkJoinPool
Cats Effect 3 の IORuntime は ForkJoinPoolベース のスケジューラを利用しています
理由
Fiber(軽量スレッド)を効率的にスケジュールできる
parTraverse, IO.race, IO.start などが高速に動く
ブロッキングI/Oは別プールに逃がせる (blocking pool)
つまり
Cats Effect の Fiber = ForkJoinPoolのタスク
Cats Effect の Scheduler = Work-Stealing実行エンジン