スレッドプール
事前に生成したスレッドを再利用し、スレッド生成のオーバーヘッドを削減する並行処理パターン
並行処理パフォーマンス
スレッドプールとは
スレッドプールは、事前に一定数のスレッドを生成しておき、タスクが到着するとプールからスレッドを割り当てて実行し、完了後にスレッドをプールに返却するパターンである。リクエストごとにスレッドを生成・破棄するオーバーヘッドを回避し、同時実行数を制御する。
仕組み
タスクキュー: [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();
現場での応用を知るには関連書籍も役立つ。