404 Not Found

404 Not Found


nginx

非同期プログラミングの徹底解説:イベントループとタスクキュー

1. 事例:200ミリ秒でブロックされたサービス

ボブのAPIサービスは通常、わずか5ミリ秒で応答するが、ピーク時には応答時間が2,000ミリ秒にまで急上昇した。ユーザーからの苦情が相次ぎ、当初彼はデータベースの処理が遅いのだと考えたが、インデックスを追加しても改善されなかった。その後、console.time を使って段階的にトラブルシューティングを行ったところ、大規模な同期型 JSON 配列のソートに 200 ms かかっていることが判明しました。この 200 ms の処理がイベントループをブロックしており、キューに蓄積されたすべてのリクエストがその処理が完了するのを待たされていたのです。イベントループの仕組みを理解した後、ボブはsetImmediateを使用してソート処理を小さなチャンクに分割し、順次実行するようにしました。その結果、ピーク時の応答時間を50ミリ秒まで短縮することができました。

学習内容


2. イベントループの6つの段階

Node.jsのイベントループはlibuvによって駆動され、6つのフェーズを順に経過します。各ループサイクルは「タイマー」から始まり、「クローズコールバック」で終了し、その後「タイマー」に戻って次のサイクルを開始します。

100%
flowchart TD
    A["timers<br/>setTimeout / setInterval"] --> B["pending callbacks<br/>System-Level Callbacks"]
    B --> C["idle, prepare<br/>For internal use only"]
    C --> D["poll<br/>I/O callbacks / new I/O polling"]
    D --> E["check<br/>setImmediate"]
    E --> F["close callbacks<br/>socket.on('close')"]
    F --> A
フェーズ コールバックの種類 説明
タイマー setTimeout / setInterval タイマーの期限が切れた時点でコールバックを実行する(最低1ミリ秒の遅延が必要)
保留中のコールバック システムレベルのコールバック 次のループに延期されたI/Oコールバック(例:TCPエラー)
アイドル、準備中 内部使用 libuvによる内部使用。開発者は通常、これとやり取りすることはない
ポーリング I/O コールバック 新しい I/O イベントの有無を確認し、I/O 関連のコールバックを実行します。タイマーがない場合、プロセスはこの段階でブロックされます
check setImmediate この段階で setImmediate のコールバックが実行されます
クローズコールバック クローズイベント 例:socket.on('close') クローズコールバック

3. マイクロタスクとマクロタスク

(1) (3.1) マイクロタスクキュー

マイクロタスクは、現在のフェーズが終了した後、次のフェーズが始まる前に、まとめて実行されます。マイクロタスクのキューには、2つの優先度レベルがあります:

マイクロタスク 優先度 説明
process.nextTick 最大値 各フェーズの終了後に nextTick キューのクリアを優先する
Promise.then / catch / finally 2番目に多い nextTickキューがクリアされた後に実行される

▶ 例:nextTickPromise.then より優先されます

JAVASCRIPT
process.nextTick(() => {
  console.log('nextTick 1');
});

Promise.resolve().then(() => {
  console.log('promise.then 1');
});

process.nextTick(() => {
  console.log('nextTick 2');
});

Promise.resolve().then(() => {
  console.log('promise.then 2');
});
TEXT
nextTick 1
nextTick 2
promise.then 1
promise.then 2

(2) (3.2) マクロタスクキュー

マクロタスクはイベントループの各フェーズごとにスケジュールされ、各フェーズはそれに対応するマクロタスクを実行します。

マクロタスク フェーズ 説明
setTimeout / setInterval タイマー 最小遅延は 1 ms。期限が切れるとタイマーキューに入る
setImmediate check 次の反復の「check」フェーズ中に実行される
I/O コールバック poll ファイルの読み書きやネットワーク操作などの I/O 操作が完了した際のコールバック
コールバックの終了 コールバックの終了 イベントの終了コールバック

4. setTimeout 対 setImmediate 対 process.nextTick

▶ 例:外側のコンテキストの実行順序は未定義です

JAVASCRIPT
setTimeout(() => {
  console.log('setTimeout');
}, 0);

setImmediate(() => {
  console.log('setImmediate');
});

I/O 以外のコンテキストでは、setTimeoutsetImmediate の実行順序は未定義です。これはシステムのプロセススケジューリングに依存し、どちらが先に実行されるかは定かではありません。

▶ 例:I/O コンテキストでは、setImmediate が常に最初に実行されます

JAVASCRIPT
const fs = require('fs');

fs.readFile(__filename, () => {
  setTimeout(() => {
    console.log('setTimeout');
  }, 0);

  setImmediate(() => {
    console.log('setImmediate');
  });
});
TEXT
setImmediate
setTimeout

I/O コールバックはポーリングフェーズ中に実行されます。これらが完了すると、処理はチェックフェーズ(setImmediate)に移行し、その後に初めてタイマーフェーズの次のラウンド(setTimeout)に進みます。

▶ 例:process.nextTick は常に先頭にくる

JAVASCRIPT
setTimeout(() => {
  console.log('setTimeout');
}, 0);

setImmediate(() => {
  console.log('setImmediate');
});

process.nextTick(() => {
  console.log('nextTick');
});
TEXT
nextTick
setTimeout   (or setImmediate, depends on scheduling)
setImmediate
機能 setTimeout(fn, 0) setImmediate process.nextTick
キュー マクロタスク(タイマー) マクロタスク(チェック) マイクロタスク(最優先)
実行フェーズ タイマー チェック 現在のフェーズが終了した直後
最小レイテンシ 1 ms なし なし
I/O コンテキストの順序 setImmediate の後 setTimeout の前 最初
外部コンテキストの順序 未定義 未定義 最も早い

5. マイクロタスクとマクロタスクの比較

次元 マイクロタスク マクロタスク
代表例 process.nextTick、Promise.then setTimeout、setImmediate、I/O コールバック
実行タイミング 現在のフェーズが終了した後、次のフェーズが始まる前 イベントループの対応するフェーズ中
実行戦略 すべてを一括でクリア フェーズごとに1つずつ実行し、その後マイクロタスクを確認
ネスト 実行中に生成されたマイクロタスクもこのラウンドでクリアされる 実行を後の段階に先送りされる
優先度 すべてのマクロタスクより高い マイクロタスクより低い

▶ 例:マクロタスクの合間に挟まれるマイクロタスク

JAVASCRIPT
setTimeout(() => {
  console.log('timeout 1');
  Promise.resolve().then(() => {
    console.log('promise between 1 and 2');
  });
}, 0);

setTimeout(() => {
  console.log('timeout 2');
}, 0);
TEXT
timeout 1
promise between 1 and 2
timeout 2

各マクロタスクが完了した後、次のマクロタスクが実行される前に、マイクロタスクのキューはクリアされます。


6. イベントループをブロックすることの危険性と、それを回避する方法

▶ 例:イベントループのブロック

JAVASCRIPT
const http = require('http');

const server = http.createServer((req, res) => {
  const start = Date.now();
  while (Date.now() - start < 200) {}
  res.end('done');
});

server.listen(3000, () => {
  console.log('Server running on port 3000');
});

各リクエストはイベントループを200ミリ秒間ブロックし、その間、他のリクエストは処理されません。

▶ 例:setImmediate を使った長時間実行タスクの分割

JAVASCRIPT
function chunkedSort(arr, compareFn, callback) {
  const CHUNK = 1000;
  let offset = 0;

  function sortChunk() {
    const end = Math.min(offset + CHUNK, arr.length);
    for (let i = offset; i < end; i++) {
      for (let j = i + 1; j < end; j++) {
        if (compareFn(arr[i], arr[j]) > 0) {
          [arr[i], arr[j]] = [arr[j], arr[i]];
        }
      }
    }
    offset = end;
    if (offset < arr.length) {
      setImmediate(sortChunk);
    } else {
      callback(arr);
    }
  }

  setImmediate(sortChunk);
}

chunkedSort(
  [5, 3, 8, 1, 9, 2, 7, 4, 6],
  (a, b) => a - b,
  (result) => {
    console.log('Sorted:', result);
  }
);
次元 ブロッキングコード ノンブロッキングコード
計算方法 単一の同期ループで完了 setImmediate を使用して小さなチャンクに分割し、順次実行
イベントループ 停止中;他のリクエストは保留中 スライス間で制御が譲渡される;他のコールバックが実行可能
応答時間 累積遅延 遅延のばらつき(ユーザーが許容できる範囲)
CPU使用率 シングルコア 100%(急上昇) 使用率が分散しており、I/O処理の時間が確保されている
ユースケース 非常に短い計算 長い計算、高並行性サービス

7. 総合的な例:イベントループの実行順序を確認するスクリプト

以下のスクリプトは、setTimeoutprocess.nextTicksetImmediatePromise.then → I/O コールバックという完全な実行順序を検証します。

JAVASCRIPT
const fs = require('fs');

console.log('--- Script start ---');

setTimeout(() => {
  console.log('1. setTimeout (macrotask - timers)');
}, 0);

setImmediate(() => {
  console.log('2. setImmediate (macrotask - check)');
});

process.nextTick(() => {
  console.log('3. process.nextTick (microtask - highest)');
});

Promise.resolve().then(() => {
  console.log('4. Promise.then (microtask)');
});

fs.readFile(__filename, () => {
  console.log('5. I/O callback (poll phase)');

  setTimeout(() => {
    console.log('6.   inner setTimeout');
  }, 0);

  setImmediate(() => {
    console.log('7.   inner setImmediate');
  });

  process.nextTick(() => {
    console.log('8.   inner nextTick');
  });

  Promise.resolve().then(() => {
    console.log('9.   inner Promise.then');
  });
});

console.log('--- Script end ---');
TEXT
--- Script start ---
--- Script end ---
3. process.nextTick (microtask - highest)
4. Promise.then (microtask)
1. setTimeout (macrotask - timers)
2. setImmediate (macrotask - check)
5. I/O callback (poll phase)
8.   inner nextTick
9.   inner Promise.then
7.   inner setImmediate
6.   inner setTimeout

実行順序の分析

  1. 同期コードが最初に実行され、「Script start」と「Script end」が出力される
  2. 現在のフェーズが終了したらマイクロタスクをクリアする:nextTick → Promise.then
  3. 「タイマー」の段階に入る:setTimeout
  4. チェックフェーズに入る:setImmediate
  5. I/Oが完了した後、ポーリングフェーズ中にコールバックを実行する
  6. I/O コールバックが完了したら、マイクロタスクをクリアする:inner nextTick → inner Promise.then
  7. 投票が終了したら、次のコードを入力してください:inner setImmediate
  8. 次のタイマーの実行:内部の setTimeout

❓ よくある質問

Q setTimeout(fn, 0) は本当に 0 ms 後に実行されるのですか?
A いいえ。ブラウザも Node.js も最小遅延(約 1 ms)があり、イベントループが「タイマー」フェーズに入るまで実行は待機する必要があります。実際の待機時間は、現在のフェーズによって異なります。
Q process.nextTickPromise.then のどちらが先に実行されますか?
A process.nextTick が先に実行されます。nextTick はマイクロタスクキュー内で最優先であり、nextTick キューは常に Promise キューよりも先に処理されます。
Q I/Oコールバックにおいて、なぜsetImmediatesetTimeoutよりも先に実行されるのですか?
A I/Oコールバックはpollフェーズ中に実行されます。そのフェーズが終了すると、プロセスは直接checkフェーズ(setImmediate)に移行し、その後、次のtimersフェーズ(setTimeout)へと進みます。したがって、I/Oのコンテキストでは、setImmediateは常にsetTimeoutよりも先に実行されます。
Q イベントループはシングルスレッドですか、それともマルチスレッドですか?
A JavaScriptの実行自体はシングルスレッドですが、基盤となるlibuvには、DNSルックアップやファイルI/Oなどの処理を扱うスレッドプール(デフォルトで4スレッド)があります。ネットワークI/Oはオペレーティングシステムのカーネルによって処理され、処理が完了するとイベントループに通知されます。
Q イベントループのブロックを避けるにはどうすればよいですか?
A 長い計算処理を小さな単位に分割し、setImmediate を使用して段階的に実行します。また、ワーカースレッドを使用して、CPU負荷の高いタスクを別のスレッドに移します。さらに、同期I/Oの代わりに非同期I/Oを使用し、ストリームを活用して大規模なデータセットを分割して処理します。
Q process.nextTick を無制限に再帰的に呼び出すことはできますか?
A 推奨されません。Node.js には process.maxTickDepth によって設定されたデフォルトの制限があります。nextTick への再帰的な呼び出しは、イベントループが次のフェーズに進むのを妨げ、I/O スターベーションを引き起こします。再帰的な nextTick の呼び出しの代わりに、setImmediate を使用すべきです。

📖 まとめ


📝 練習問題

  1. このレッスンにあるすべてのコード例を完成させ、それぞれが正しく動作することを確認してください。
  2. 包括的な例を修正し、独自の拡張機能を追加する
  3. 公式ドキュメントを確認し、このレッスンで扱われていないAPIを1~2つ見つけ、それらのテストコードを作成してください。
  4. 振り返り:このレッスンで学んだことを、実際のプロジェクトにどのように活かしますか?
  5. このレッスンで学んだことと、これまでのレッスンの内容を組み合わせて、小さなプロジェクトを作成してみてください。
Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%