カテゴリー
【nodejs基礎講座】Node.jsを組込みアプリケーションに使う前に知っておきたい「イベントループ」の仕組み
※ 当ページには【広告/PR】を含む場合があります。
2024/04/23
Node.jsの内部アーキテクチャ〜libuvの仕組み
libuvとは?
epoll (Linux)
kqueue (BSD)
event port (Solaris)
IOCP (Windows)
Node.jsのイベントループ
libuvのイベントループ
1. タイマーがあればスケジュールを設定
2. process.nextTick関数等のマイクロキューの残りを実行
3. 同期タスクがあれば実行
4. 非同期APIのコールバック呼び出し
順序 | フェイズ名 | イベントキュー名 | 概要 |
---|---|---|---|
1 | timer | Expired Timers/Intervals Queue | setTimetout等でスケジュールされたコールバックがタイムアウトした場合に実行 |
2 | pending | -- | 一つ前のループで何らかの理由で実行が延期されたコールバックを実行 |
3 | idle/prepare | -- | 内部処理を実行 |
4 | poll | I/O Events Queue | I/Oイベントを取得・関連のコールバックを実行 |
5 | check | Immediates Queue | setImmediate等を実行 |
6 | close | Close Handlers Queue | close系イベントのコールバックを実行 |
1. キューにあるジョブを全て実行する(=Emptyになる)
2. キューにあるコールバックの数が最大数(上限)に達する
「マイクロタスク」
「マイクロタスクキュー」
「nextTickQueue」
「microTaskQueue」
順序 | マイクロタスク | キュー名 | 概要 |
---|---|---|---|
1 | nextTick | nextTickQueue | Promise系の非同期処理よりも先行して処理を実行 |
2 | microTask | microTaskQueue | PromiseなどJavascriptビルドインの非同期処理を実行 |
リアクターパターン
実際のJavascriptコードでイベントループの処理を確認する
$ node --version
v20.12.2
setTimeout関数を利用する際の注意点
setTimeout
setTimeout
1 - 2147483647[ms]
1[ms]
setTimeout(() => {console.log('1msくらい遅延!'), 1});
setTimeout(() => {console.log('1msくらい遅延!')});
setTimeout(() => {console.log('1msくらい遅延!'), 0});
setTimeout(() => {console.log('1msくらい遅延!'), -1});
setTimeout(() => {console.log('1msくらい遅延!'), undefined});
setTimeout(() => {console.log('1msくらい遅延!'), 100_000_000_000});
//👇ちょっと長め(=2ms)のタイマー
setTimeout(() => console.log(`2msでTimerフェイズです`), 2);
for (let i=0; i < 5;i++) {
//👇ちょっと長め(=2ms)のタイマー
setTimeout(() => {
console.log(`1msでTimerフェイズ${i}です`);
//👇微妙な計算負荷をかけて若干遅延気味に
for (let j = 0; j < 100_000_000; j++) {}
});
}
setImmediate(() => console.log('Checkフェイズ'));
1msでTimerフェイズ0です
1msでTimerフェイズ1です
1msでTimerフェイズ2です
1msでTimerフェイズ3です
1msでTimerフェイズ4です
Checkフェイズ
2msでTimerフェイズです
1msでTimerフェイズ0です
1msでTimerフェイズ1です
Checkフェイズ
2msでTimerフェイズです
1msでTimerフェイズ2です
1msでTimerフェイズ3です
1msでTimerフェイズ4です
Checkフェイズ
2msでTimerフェイズです
1msでTimerフェイズ0です
1msでTimerフェイズ1です
1msでTimerフェイズ2です
1msでTimerフェイズ3です
1msでTimerフェイズ4です
同期処理・Timer/Pending/Closeフェイズ・マイクロタスクの確認
同期処理
Timerフェイズ
Pendingフェイズ
Closeフェイズ
マイクロタスク
import { fileURLToPath } from "node:url";
import path from "node:path";
import fs from "node:fs";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
console.log('同期処理1です');
setTimeout(() => {
console.log('Timerフェイズ1です');
process.nextTick(() => console.log('マイクロタスク3です'));
Promise.resolve().then(() => console.log('マイクロタスク4です'));
});
setImmediate(() => {
process.nextTick(() => console.log('マイクロタスク5です'));
Promise.resolve().then(() => console.log('マイクロタスク6です'));
console.log('Checkフェイズ1です');
});
process.nextTick(() => console.log('マイクロタスク1です'));
Promise.resolve().then(() => console.log('マイクロタスク2です'));
console.log('同期処理2です');
//👇存在しないファイルを読み込むためErrorレスポンスが返る
fs.readFile(`not-found.txt`, () => {
process.nextTick(() => console.log('マイクロタスク7です'));
Promise.resolve().then(() => console.log('マイクロタスク8です'));
console.log('Pendingフェイズ1です');
setTimeout(() => {
console.log('Timerフェイズ2です');
process.nextTick(() => console.log('マイクロタスク9です'));
Promise.resolve().then(() => console.log('マイクロタスク10です'));
});
});
//👇jsファイルと同じフォルダにあるsample.txtファイルを読み込む
const readStream = fs.createReadStream(`${__dirname}/sample.txt`);
readStream.on("data", () => {
console.log('Pendingフェイズ2です');
process.nextTick(() => console.log('マイクロタスク11です'));
Promise.resolve().then(() => console.log('マイクロタスク12です'));
}).on('close', () => {
console.log('Closeフェイズ1です');
Promise.resolve().then(() => console.log('マイクロタスク14です'));
process.nextTick(() => console.log('マイクロタスク13です'));
});
Promise.resolve().then(() => console.log('マイクロタスク15です'));
console.log('同期処理3です');
process.nextTick(() => console.log('マイクロタスク16です'));
同期処理1です
同期処理2です
同期処理3です
マイクロタスク1です
マイクロタスク16です
マイクロタスク2です
マイクロタスク15です
Timerフェイズ1です
マイクロタスク3です
マイクロタスク4です
Pendingフェイズ1です
マイクロタスク7です
マイクロタスク8です
Checkフェイズ1です
マイクロタスク5です
マイクロタスク6です
Timerフェイズです2
マイクロタスク9です
マイクロタスク10です
Pendingフェイズ2です
マイクロタスク11です
マイクロタスク12です
Closeフェイズ1です
マイクロタスク13です
マイクロタスク14です
同期処理1です
同期処理2です
同期処理3です
マイクロタスク1です
マイクロタスク16です
マイクロタスク2です
マイクロタスク15です
nextTick
Promise
Timerフェイズ1です
マイクロタスク3です
マイクロタスク4です
Pendingフェイズ1です
マイクロタスク7です
マイクロタスク8です
Checkフェイズ1です
マイクロタスク5です
マイクロタスク6です
Timer --> Pending --> Check
fs.readFile
Timerフェイズ2です
マイクロタスク9です
マイクロタスク10です
Pendingフェイズ2です
マイクロタスク11です
マイクロタスク12です
Closeフェイズ1です
マイクロタスク13です
マイクロタスク14です
fs.createReadStream
Timer --> Pending --> Close
Pollフェイズの確認
Idle/Prepare
Poll
Idle/Prepare
Idle/Prepare
Poll
Poll
1. TimersキューかCheckキューが空でない(スケジューリングされている)場合、
Pollフェーズを一旦終了し、次のCheckフェーズへ進む
2. TimersキューとCheckキューが空になっている(スケジューリングされていない)場合、
Pollフェイズでキューが空になるまで待ち続ける
import { fileURLToPath } from "node:url";
import fs from "node:fs";
const __filename = fileURLToPath(import.meta.url);
setTimeout(() => console.log('Timerフェイズ1です'));
setImmediate(() => console.log('Checkフェイズ1です'));
fs.readFile(`not-found.txt`, () => {
console.log('Pendingフェイズ1です');
setTimeout(() => console.log('Timerフェイズ2です'));
setImmediate(() => console.log('Checkフェイズ2です'));
});
fs.readFile(__filename, () => {
console.log('Pendingフェイズ2です');
setImmediate(() => console.log('Checkフェイズ3です'));
});
Timerフェイズ1です
Pendingフェイズ1です
Checkフェイズ1です
Checkフェイズ2です
Timerフェイズ2です
Pendingフェイズ2です
Checkフェイズ3です
import { fileURLToPath } from "node:url";
import fs from "node:fs";
const __filename = fileURLToPath(import.meta.url);
setTimeout(() => console.log('Timerフェイズ1です'));
setImmediate(() => console.log('Checkフェイズ1です'));
fs.readFile(__filename, () => {
console.log('Pendingフェイズ1です');
setTimeout(() => console.log('Timerフェイズ2です'));
setImmediate(() => console.log('Checkフェイズ2です'));
});
fs.readFile(__filename, () => {
console.log('Pendingフェイズ2です');
setImmediate(() => console.log('Checkフェイズ3です'));
});
Timerフェイズ1です
Checkフェイズ1です
Pendingフェイズ1です
Pendingフェイズ2です
Checkフェイズ2です
Checkフェイズ3です
Timerフェイズ2です
Pollフェイズの特殊ルール
fs.readFile
fs.readFile
setTimeout
Timerフェイズ2です
fs.readFile
setTimeout
fs.readFile
fs.readFile
まとめ
記事を書いた人
ナンデモ系エンジニア
主にAngularでフロントエンド開発することが多いです。 開発環境はLinuxメインで進めているので、シェルコマンドも多用しております。 コツコツとプログラミングするのが好きな人間です。
カテゴリー