イベントループ
Node.js のシングルスレッドで非同期 I/O を実現する実行モデル
イベントループとは
イベントループは、Node.js のシングルスレッドで非同期 I/O を実現する実行モデルである。コールスタック、タスクキュー、マイクロタスクキューを巡回し、非同期コールバックを順番に実行する。
動作の流れ
イベントループは、実行中の同期コードが終わってコールスタックが空になるたびに、まずマイクロタスクキュー (Promise の .then など) を空になるまで処理し、そのあとタイマーや I/O のコールバックへ移る。この「同期 → マイクロタスク → それ以外」という優先順位が骨格で、シングルスレッドでも非同期処理をさばけるのはこの巡回があるからである。
ただし「キューから 1 つ取り出したら次の周回へ」という説明はブラウザー側のモデルに近い簡略化で、Node.js の実装は後述のフェーズ単位で動く。Node.js 公式ドキュメントによれば、各フェーズは自分の FIFO キューを空になるまで (またはコールバック数の上限に達するまで) 処理してから次のフェーズへ進む。したがって「1 周回 = コールバック 1 つ」ではない。
1. コールスタックが空か確認
2. マイクロタスクキュー (Promise) を全て実行
3. タイマー・I/O のコールバックを処理 (Node.js はフェーズ単位でキューを空にする)
4. 1 に戻る
┌─────────────────────────┐
│ コールスタック │ ← 同期コードを実行
└────────────┬────────────┘
↓
┌─────────────────────────┐
│ マイクロタスクキュー │ ← Promise.then, queueMicrotask
└────────────┬────────────┘
↓
┌─────────────────────────┐
│ タスクキュー │ ← setTimeout, setInterval, I/O
└─────────────────────────┘
実行順序
実行順序のコード例を示す。
console.log('1'); // 同期
setTimeout(() => console.log('2'), 0); // タスクキュー
Promise.resolve().then(() => console.log('3')); // マイクロタスク
console.log('4'); // 同期
// 出力: 1, 4, 3, 2
// 同期 → マイクロタスク → タスクキュー の順
Node.js のイベントループフェーズ
Node.js のイベントループフェーズを以下にまとめる。
| フェーズ | 処理内容 |
|---|---|
| timers | setTimeout, setInterval のコールバック |
| pending callbacks | 前の周回から繰り越された一部のシステム操作のコールバック (例: TCP 接続が ECONNREFUSED を受け取ったときのエラー通知) |
| poll | 新しい I/O イベントの取得と、大半の I/O コールバックの実行 (close 系・タイマー・setImmediate は除く) |
| check | setImmediate のコールバック |
| close callbacks | socket.on('close') 等、一部の close コールバック |
実務で意識するのはこの 5 つで、公式ドキュメントの図にある idle, prepare は Node.js が内部的に使うだけである。名前から「I/O コールバックは pending callbacks で走る」と読みたくなるが、実際に大半の I/O コールバックが走るのは poll フェーズで、pending callbacks は前の周回で報告を後回しにしたものを片付ける場所である。
ここから実務上の落とし穴が 1 つ導ける。poll フェーズのコールバックが長引くと、その間に満期を迎えたタイマーはフェーズが一巡するまで待たされる。公式ドキュメントも、長時間走るコールバックによって poll フェーズがタイマーの閾値より長く続き得ると明記している。setTimeout(fn, 100) は「100 ミリ秒後」ではなく「最短 100 ミリ秒後」であり、遅延の精度を前提にした実装 (アニメーションの間隔・タイムアウト判定の境界) は避ける。
ブロッキングの問題
ブロッキングの問題のコード例を示す。
// ❌ イベントループをブロック (他のリクエストが処理できない)
function heavyComputation() {
for (let i = 0; i < 1e9; i++) { /* CPU 集約的な処理 */ }
}
// ✅ Worker Threads で別スレッドに逃がす
import { Worker } from 'worker_threads';
const worker = new Worker('./heavy.js');
シングルスレッド vs マルチスレッド
シングルスレッドとマルチスレッドの違いを以下にまとめる。
| 観点 | Node.js (イベントループ) | Java (スレッドプール) |
|---|---|---|
| I/O 処理 | 高速 (ノンブロッキング) | スレッドごとにブロック |
| CPU 処理 | 苦手 (シングルスレッド) | 得意 (マルチスレッド) |
| メモリ | 少ない | スレッドごとにスタック |
| 複雑さ | コールバック/Promise | デッドロック、競合 |
詳しくは関連書籍を参照。
この記事は役に立ちましたか?
関連用語
非同期プログラミング
I/O 待ちの間に他の処理を進め、システムのスループットを向上させるプログラミング手法
goroutine
Go の軽量スレッドで、数十万規模の並行処理を低コストで実現する
プロセスとスレッドの違いとは - メモリ空間 / 生成コスト / 通信方法を比較
プロセスとスレッドの違いをメモリ空間 / 生成コスト / 通信方法の 3 軸で比較。Node.js での使い分け、Lambda の実行環境、データ競合とコンテナの PID 1 の落とし穴まで解説
スレッドプール
事前に生成したスレッドを再利用し、スレッド生成のオーバーヘッドを削減する並行処理パターン
Node.js
Chrome V8 エンジン上で動作するサーバーサイド JavaScript ランタイム
並行処理 (Concurrency) とは - 並列処理 (Parallelism) との違い
Concurrency (並行処理) の意味を解説。複数のタスクを論理的に同時進行させる手法で、物理的に同時実行する並列処理 (Parallelism) とは区別される。Node.js / Go / Rust の並行処理モデルの違いも紹介。
関連する記事
技術書と公式ドキュメントの使い分け - それぞれの強みを活かす
技術書と公式ドキュメントの役割の違いを明確にし、学習段階に応じた最適な使い分け方を紹介します。
「あとで読む」ブックマークが 100 件を超えたら本を買え
ブラウザのブックマークに技術記事を溜め込んでいませんか。断片的な記事を 100 件読むより、同じテーマの本を 1 冊読む方が効率的な理由を解説します。
ゼロから作る Deep Learning シリーズの読む順番 - 全 6 巻の内容と選び方を整理
ゼロから作る Deep Learning シリーズ全 6 巻 (基礎 / 自然言語処理 / フレームワーク / 強化学習 / 生成モデル / LLM) の読む順番を解説。各巻の内容 / 発売年 / 前提知識を一覧表で整理し、目的別にどの巻から読むべきかを案内します。