イベントループ

Node.js のシングルスレッドで非同期 I/O を実現する実行モデル

Node.js非同期

イベントループとは

イベントループは、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 のイベントループフェーズを以下にまとめる。

フェーズ処理内容
timerssetTimeout, setInterval のコールバック
pending callbacks前の周回から繰り越された一部のシステム操作のコールバック (例: TCP 接続が ECONNREFUSED を受け取ったときのエラー通知)
poll新しい I/O イベントの取得と、大半の I/O コールバックの実行 (close 系・タイマー・setImmediate は除く)
checksetImmediate のコールバック
close callbackssocket.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デッドロック、競合

詳しくは関連書籍を参照。

この記事は役に立ちましたか?

関連用語

関連する記事