スレッドプール
事前に生成したスレッドを再利用し、スレッド生成のオーバーヘッドを削減する並行処理パターン
スレッドプールとは
スレッドプールは、事前に一定数のスレッドを生成しておき、タスクが到着するとプールからスレッドを割り当てて実行し、完了後にスレッドをプールに返却するパターンである。リクエストごとにスレッドを生成・破棄するオーバーヘッドを回避し、同時実行数を制御する。
仕組み
実体は「固定数のスレッド」と「タスクキュー」の組である。スレッドはキューの先頭からタスクを 1 つ取り出して実行し、終わったらまたキューを見に行く。この構造だけで、生成コストの削減と同時実行数の上限という 2 つの役目を同時に果たす。
タスクの到着 → [ タスクキュー ] Task6 Task5 Task4
|
+---------------+---------------+
[Thread1] [Thread2] [Thread3] ← 固定 3 スレッド
Task1 実行中 Task2 実行中 Task3 実行中
+--- 完了したらキューから次の 1 件を取る ---+
設計で最初に決めるべきなのは、タスクの到着が処理を追い越したときの振る舞いである。キューが無制限なら、待ちタスクがメモリに積み上がり続けて最後にプロセスが落ちる。キューに容量を決めておけば、満杯になった時点で投入側が待たされるか拒否され、負荷が上流へ押し戻る。後者を選ぶのが、あとから効いてくる判断になる。
Node.js の Worker Thread プール
Node.js が単一スレッドで回しているのは JavaScript の実行 (イベントループ) であり、プロセス自体は複数のスレッドを持つ。画像変換や暗号計算のようにイベントループを長く占有する処理は、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 スレッドで、ここへ回されるのはファイルシステム API のほぼ全部 (同期版と fs.FSWatcher() を除く)、dns.lookup() と dns.lookupService()、crypto.pbkdf2() や crypto.scrypt() のような重い暗号処理、そして zlib の非同期 API である。
逆に、TCP や HTTP のネットワーク I/O はこのプールを通らない。OS が用意する非同期通知をイベントループが直接受け取るため、接続 1 本ごとにスレッドを占有しない。「非同期処理は全部プールで動いている」と誤解すると、サイズを増やしても何も速くならない場面で調整に時間を使うことになる。
# libuv のスレッドプールサイズを変更
UV_THREADPOOL_SIZE=8 node app.js
この環境変数は起動時にだけ読まれる。プールはユーザーコードより先に作られるので、実行中に process.env へ代入しても効かない。指定できる上限は 1024 で、libuv 1.30.0 で 128 から引き上げられた。
ファイル I/O や pbkdf2 の呼び出しが多いアプリケーションでは、既定の 4 スレッドがボトルネックになることがある。ただしプールが初めて使われた時点で指定数のスレッドをまとめて作るため、増やせばその分のスレッドスタックを抱える。効果と引き換えを計測しながら動かす値である。
プールサイズの設計
判断の軸は 1 つで、スレッドが実際に CPU を使っている時間の割合である。計算しかしないタスクはコア数を超えて増やしても実行待ちが増えるだけだが、待ち時間の長いタスクなら待っている間に別のスレッドが走れるため、コア数より多いほうが速くなる。
| ワークロード | サイズの目安 | 理由 |
|---|---|---|
| CPU バウンド | CPU コア数 | コア数以上にしてもコンテキストスイッチが増えるだけ |
| I/O バウンド | CPU コア数の 2〜4 倍 | 待ち時間と計算時間の比が大きいほど、増やす余地も大きい |
| 混合 | 計測して調整 | プロファイリングで最適値を決定 |
倍率は最初の当たりを付けるための数字で、正解ではない。動かす前に、飽和しているのがプールなのか、その先のディスク・DB・外部 API なのかを切り分ける。後者ならスレッドを増やしても待ち行列の置き場所が変わるだけで終わる。
DB コネクションプールとの関係
DB コネクションプールはスレッドプールの応用ではなく、「作り直さずに再利用する」という同じ考え方 (オブジェクトプール) を TCP 接続に当てたものである。接続確立のコスト (TCP ハンドシェイク、TLS、認証) を毎回払わずに済む。
この 2 つのプールは独立に決められない。スレッド数を接続数より大きくすると、あふれたスレッドは接続の空きを待つだけになり、待ち行列がスレッドプールから接続プールへ移動して終わる。Lambda のように実行環境が横に大量に並ぶ構成では DB 側の接続上限が先に尽きるため、RDS Proxy のような外部のプールへ接続の管理を寄せる。
Java の ExecutorService
Java ではスレッドプールの窓口が ExecutorService になる。submit() がタスクを預かって Future を返し、get() が完了まで待って結果を取り出す。get() は検査例外を 2 つ投げるため、呼び出し側は必ずその処理を書くことになる。
ExecutorService pool = Executors.newFixedThreadPool(4);
try {
Future<String> future = pool.submit(() -> {
return processData();
});
String result = future.get(); // 完了までブロックする
System.out.println(result);
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // 割り込み状態を戻してから抜ける
} catch (ExecutionException e) {
// タスクが投げた例外は ExecutionException に包まれて届く
System.err.println("タスク失敗: " + e.getCause());
} finally {
pool.shutdown(); // 新規受付を止める (実行中のタスクは完了を待つ)
}
落とし穴は newFixedThreadPool() の内側にある。このファクトリが作るのは共有の無制限キューを持つプールなので、投入が処理より速い状態が続くとキューが際限なく伸びる。流量に上限を設けたいなら ThreadPoolExecutor を直接組み立て、容量を決めた BlockingQueue を渡す。満杯時の既定の振る舞いは AbortPolicy で、RejectedExecutionException を投げて投入側に失敗を伝える。
現場での応用を知るには関連書籍も役立つ。
この記事は役に立ちましたか?
関連用語
オブジェクトプールパターン
生成コストの高いオブジェクトを事前に確保してプールし、再利用することでパフォーマンスを向上させるパターン
Worker Threads
Node.js で CPU 集約的な処理をメインスレッドをブロックせずに別スレッドで実行する仕組み
並行処理 (Concurrency) とは - 並列処理 (Parallelism) との違い
Concurrency (並行処理) の意味を解説。複数のタスクを論理的に同時進行させる手法で、物理的に同時実行する並列処理 (Parallelism) とは区別される。Node.js / Go / Rust の並行処理モデルの違いも紹介。
goroutine
Go の軽量スレッドで、数十万規模の並行処理を低コストで実現する
プロセスとスレッドの違いとは - メモリ空間 / 生成コスト / 通信方法を比較
プロセスとスレッドの違いをメモリ空間 / 生成コスト / 通信方法の 3 軸で比較。Node.js での使い分け、Lambda の実行環境、データ競合とコンテナの PID 1 の落とし穴まで解説
Go
Google が開発したシンプルで高速なコンパイル言語で、goroutine による軽量な並行処理が特徴
関連する記事
DevOps 本ガイド - CI/CD とインフラ自動化を学ぶ技術書の選び方
DevOps の文化と原則から CI/CD、IaC、オブザーバビリティまで学べる技術書の選び方と学習順序を紹介します。
あの有名 OSS のコードは、この本の影響を受けている
広く使われているオープンソースソフトウェアの設計には、技術書と共通の語彙や原則が残っています。OSS のコードから辿れる影響と、似ているだけの例の見分け方を整理しました。
コードを書かずに技術力を上げる休日の過ごし方
休日にコードを書く気力がないとき、それでも技術力を伸ばす方法があります。読書、設計スケッチ、技術記事の執筆など、キーボードに触れずにスキルアップする具体的な過ごし方。