スレッドプール

事前に生成したスレッドを再利用し、スレッド生成のオーバーヘッドを削減する並行処理パターン

並行処理パフォーマンス

スレッドプールとは

スレッドプールは、事前に一定数のスレッドを生成しておき、タスクが到着するとプールからスレッドを割り当てて実行し、完了後にスレッドをプールに返却するパターンである。リクエストごとにスレッドを生成・破棄するオーバーヘッドを回避し、同時実行数を制御する。

仕組み

タスクキュー: [Task1] [Task2] [Task3] [Task4] [Task5]
                ↓       ↓       ↓
スレッドプール: [Thread1] [Thread2] [Thread3]  ← 3 スレッド
                ↓       ↓       ↓
              Task1   Task2   Task3 を実行中
              完了 → Task4 を実行

Node.js の Worker Thread プール

Node.js はシングルスレッドだが、worker_threads でスレッドプールを実装できる。piscina ライブラリが広く使われている。

import Piscina from 'piscina';

const pool = new Piscina({
  filename: './worker.js',
  maxThreads: 4,  // プールサイズ
});

// タスクをプールに投入
const results = await Promise.all(
  images.map(img => pool.run({ path: img, width: 800 }))
);

libuv のスレッドプール

Node.js の内部では、libuv がスレッドプール (デフォルト 4 スレッド) を使って、ファイル I/O、DNS 解決、暗号化処理を非同期に実行している。

# libuv のスレッドプールサイズを変更
UV_THREADPOOL_SIZE=8 node app.js

ファイル I/O が多いアプリケーションでは、デフォルトの 4 スレッドがボトルネックになることがある。

プールサイズの設計

ワークロード 推奨サイズ 理由
CPU バウンド CPU コア数 コア数以上にしてもコンテキストスイッチが増えるだけ
I/O バウンド CPU コア数 × 2〜4 I/O 待ちの間に他のスレッドが実行できる
混合 計測して調整 プロファイリングで最適値を決定

DB コネクションプールとの関係

DB コネクションプールはスレッドプールの応用だ。TCP 接続の確立コスト (ハンドシェイク、認証) を回避し、接続を再利用する。Lambda では RDS Proxy がコネクションプーリングを提供する。

Java の ExecutorService

ExecutorService pool = Executors.newFixedThreadPool(4);
Future<String> future = pool.submit(() -> {
    return processData();
});
String result = future.get(); // 結果を待つ
pool.shutdown();

現場での応用を知るには関連書籍も役立つ。

関連用語