Node.jsのイベントループについて

Node.jsの実行原理は、イベントループを中心に構築されています。以下では、その仕組みと各フェーズの動作について説明します。

実行プロセス

  • Node.jsが起動すると、まずイベントループが初期化され、提供された入力スクリプト(同期的なメインモジュールコード)が実行されます。この過程で非同期API呼び出しやタイマー、process.nextTick()などが発生し、その後イベントループ処理が始まります。
  • Node.jsのイベントループは6つのフェーズに分かれ、順番に反復して実行されます。それぞれのフェーズに入るたびに、対応するコールバックキューから関数を取り出して実行します。
  • 各フェーズ内のマクロタスクが終了すると、process.nextTickとPromiseのマイクロタスクが実行されます。そして次のフェーズに移ります。

フェーズの概要

  • タイマーフェーズ: setTimeout()やsetInterval()によって設定されたコールバックを実行します。
  • 保留中のコールバック: 次のループイテレーションまで遅延されたI/Oコールバックを実行します。
  • アイドル/準備: 内部使用のみ。
  • ポーリング: 新しいI/Oイベントを取得し、I/O関連のコールバックを実行します(閉じるコールバックやタイマーコールバック、setImmediateコールバック以外のほぼすべてのコールバック)。特にこの「ポーリング」フェーズが重要です。
  • チェック: setImmediate()によるコールバックがここで呼び出されます。
  • クローズコールバック: ソケットなどのクローズコールバックがここに含まれます。

イベントループは、I/Oタスクがない場合、ポーリングフェーズで待機状態に入ります。新しいI/Oタスクが挿入されるまで休眠します。また、I/Oタスクがある場合、ポーリングキューが空になった後にsetImmediateキューと期限切れタイマーを確認し、存在すれば現在のフェーズを終了し次のフェーズに進みます。

主要なAPI


// チェックフェーズで実行される非同期関数
setImmediate();

// 現在のイベントループフェーズが終了した後、次のフェーズに入る前に実行される非同期関数
// 技術的にはイベントループの一部ではありません
process.nextTick();

// V8エンジンで実装されたマイクロタスク関数であり、イベントループの一部ではありません
Promise;

setImmediate()とsetTimeout()

両者の実行順序は、呼び出される文脈によって異なります。

ケース1:シンプルな例


// test.js
setTimeout(() => {
  console.log('タイムアウト');
}, 0);

setImmediate(() => {
  console.log('即時実行');
});

// 実行順序は固定されていません
// 原因:システムスケジューリングに依存(マシンの性能や他のアプリケーションによるリソース競合など)

ケース2:同期コードが長時間実行される場合


setTimeout(() => {
  console.log('タイムアウト');
}, 0);

setImmediate(() => {
  console.log('即時実行');
});

for (let i = 0; i < 1000000; i++) {
  // noop
}

// 実行順序:タイムアウト -> 即時実行
// 原因:同期コードの実行時間がタイマーの時間を超えているため、最初のイベントループでタイマーフェーズが優先される

ケース3:ESモジュールの場合


// test.mjs
setTimeout(() => {
  console.log('タイムアウト');
}, 0);

setImmediate(() => {
  console.log('即時実行');
});

// 実行順序:即時実行 -> タイムアウト
// 原因:グローバル同期コードやマイクロタスクの実行がタイマーの計時を妨げず、初回のイベントループでタイマーのコールバックがキューに追加される確率が低い

ケース4:I/Oコールバック内での比較


// test.jsまたはtest.mjs
const fs = require('node:fs');

fs.readFile(__filename, () => {
  setTimeout(() => {
    console.log('タイムアウト');
  }, 0);

  setImmediate(() => {
    console.log('即時実行');
  });
});

// 実行順序:即時実行 -> タイムアウト
// 原因:I/Oコールバック時はポーリングフェーズにあるため、次のフェーズであるチェックフェーズでsetImmediateが必ず先に実行される

タグ: Node.js EventLoop AsynchronousProgramming

7月4日 20:13 投稿