【Node.js実戦習得】第4回:Node.jsのイベントループとシングルスレッドの仕組みを理解する

社員ブログ

酒本先輩……。

次の現場に向けて必死に予習してるんですけど、やっぱりバックエンドへの不安が拭えなくて。

JavaScriptの非同期処理自体は、フロントでAPIを叩くときとかに使い慣れてるつもりなんです。

でも、ブラウザと違って、サーバーって不特定多数のユーザーから同時に大量のアクセスが飛んでくるわけじゃないですか。

1人の店員(シングルスレッド)が非同期でタスクを回しているだけだと、リクエストが何万件も重なったときに、結局どこかで順番待ちが発生してパンクしちゃうんじゃないかって思って……

なるほどね!

フロントエンドで非同期処理を書いてきた熊木くんらしい、すごく深い疑問だわ。

確かに、ただ1人でタスクを切り替えているだけなら、限界がありそうに思えるわよね。

でも大丈夫。

Node.jsが大量の同時リクエストをスイスイ捌けるのは、メインの店員がただ非同期で頑張っているからではなくて、OSや裏方のスレッドを巻き込んだ『超効率的な仕組み』で動いているからよ。

それが今回解説するイベントループノンブロッキングI/Oの正体ね

「シングルスレッド」と「マルチスレッド」の違い

熊木くんは普段Reactとかのフロントをやってるから、ブラウザがフリーズする怖さは身に染みてるわよね。

一般的なWebサーバー(Apacheなど)とNode.jsの違いを、レストランの運営に例えてみましょうか

従来のマルチスレッド型(1リクエスト = 1スレッド)

従来のサーバーは、お客さん(リクエスト)が来るたびに、新しい店員(スレッド)を1人雇って対応させるスタイルよ

それなら、処理がバッティングしなくて良さそうですね?

ええ。

でも、もし料理(DB処理やファイル読み込みなどのI/O)に時間がかかるとして、その店員が厨房の前でずっと突っ立って待っていたらどうなる?

お客さんが1万人同時に来たら?

店員を1万人雇わなきゃいけないから、メモリを消費しすぎてサーバーが破産(ダウン)しちゃいますね……!

その通り。

これを専門用語でC10K問題(クライアント1万台問題)と呼ぶの。

Node.jsのシングルスレッド型(1スレッド + イベントループ)

対するNode.jsの店員は、メインの1人(シングルスレッド)だけよ

えっ!?

バックエンドなのにワンオペですか!?

それこそパンクしませんか?

ここがポイント。

この店員は注文を受けたら、料理の完成をその場で待たないの。

奥の厨房に料理を任せて、『できたらベルで呼んで!』と言って、すぐに次のお客さんの注文を取りに行くわ

なるほど!

待つ時間をムダにせず、常に動き回ってるから1人でも回せるんだ!

そう。

そして料理が完成してベルが鳴ったら(イベント通知)、店員がお客さんに料理を運ぶ(コールバック関数の実行)。

これがNode.jsの強さの秘密よ

Node.jsの舞台裏:libuvとアーキテクチャ

ブラウザの非同期処理と似てますね。

でも酒本先輩、さっき言っていた『奥の厨房』って、Node.jsの内部では何が担当しているんですか?

いい質問ね。

Node.jsの内部は、大きく分けて2つの要素で動いているわ

 

  1. V8エンジン: Google製。JavaScriptを高速で実行する、メインの店員(シングルスレッド)ね。
  2. libuv(リブユーブイ): C言語で書かれた、非同期処理を制御する超重要ライブラリ

 

この libuv の中に、バックグラウンドで重い処理を代わりに引き受けてくれる『スレッドプール』(デフォルトで4つの裏方スレッド)があるの。

ファイルの読み書きや重い通信は、メインの店員じゃなくて、この裏方が裏でこっそり処理しているのよ

 

イベントループの「フェーズ」と実行順序

裏方が処理を終えたコールバック関数は、メインの店員はどういう順番で処理するんですか?

まさか早い者勝ちですか?

そんな雑なわけないでしょ(笑)。

イベントループは、時計回りにぐるぐると決まった『フェーズ(段階)』を周りながら、実行待ちのキュー(行列)を消化しているのよ。

代表的なのはこの4つね

フェーズ名主な役割
① TimerssetTimeout などのタイマーが満了した処理を実行する。
② Poll (I/O)ファイルの読み書き(fs)やネットワーク通信などの処理を実行する。
③ ChecksetImmediate で登録された処理を、Pollフェーズの直後に即座に実行する。
④ Close通信が切断されたときなどのクローズイベントを処理する。

さらに、各フェーズの合間に最優先で割り込んで実行される『マイクロタスク』(Promise.thenprocess.nextTick)もあるわ。

フロントエンドでもPromiseの挙動は馴染みがあるでしょ?

【実践】コードの実行順序を予測してみよう

じゃあ熊木くん、実際にイベントループを動かすevent_loop.jsを作ってみましょう!

touch event_loop.js

次のコードを書き込んでフロントエンドの知識でこのコードを動かしたとき、どんな順番でログが出るか当ててみて

// event_loop.js
const fs = require('fs');

console.log('1: スタート');

setTimeout(() => {
  console.log('2: setTimeout (0秒)');
}, 0);

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

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

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

fs.readFile(__filename, () => {
  console.log('6: ファイル読み込み完了');
  setTimeout(() => console.log('7: I/O内の setTimeout'), 0);
  setImmediate(() => console.log('8: I/O内の setImmediate'));
});

console.log('9: エンド');

書きました!

ブラウザのイベントループと同じなら……

ああっ、でも setImmediate とか process.nextTick はNode.js特有だから分からないです

ターミナルで実行してみて。

ちなみに __filenameは 現在実行中のJavaScriptファイルの絶対パス(フルパス)を保持する特殊な変数よ

node event_loop.js

実行結果

1: スタート
9: エンド
5: process.nextTick
4: Promise.then
2: setTimeout (0秒)
3: setImmediate
6: ファイル読み込み完了
8: I/O内の setImmediate
7: I/O内の setTimeout

💡 酒本先輩の解説

  1. 同期処理が最優先: まず、普通に上から順に実行されるので 1: スタート9: エンド が最初に出力されるわ。
  2. 割り込み(マイクロタスク): 同期処理が終わった瞬間、最優先マイクロタスクの process.nextTick(5)と Promise(4)が実行されるの。Node.jsでは nextTick が最優先よ。
  3. イベントループ開始: タイマー(2)と Check(3)が実行。
  4. I/Oの完了後: ファイルが読み終わると(6)が実行。Poll(I/O)フェーズの直後は必ずCheckフェーズ(setImmediate)に移動するルールだから、I/Oの内部では setTimeout(7)より setImmediate(8)が絶対に先になるのよ!

5. 実務での注意点:メインスレッドを絶対に「ブロック」してはいけない!

なるほど!

実際のファイルで試すと、Node.jsってフロントの知識も活かせて面白いですね!

そうよ。

でも、バックエンドならではの致命的な弱点があるわ。

もし、メインの店員(1人)が、『1千万回ループする超巨大な計算』を始めたらどうなると思う?

あ……!

店員がその計算にかかりきりになって、次のお客さんの注文(リクエスト)を一切受け付けられなくなりますか……?

その通り。

全ユーザーを巻き添えにしてサーバーが完全にフリーズするわ。

だから、Node.jsでは『時間のかかる同期処理(for文の乱用や、fs.readFileSyncなど)』は絶対にNG。

次の現場でこれやったら一発で怒られるから、今ここで肝に銘じてね。

恐ろしいですね……。

絶対に気をつけます!

本日のまとめ

  1. イベントループ: メインの店員は1人(シングルスレッド)だけど、重い処理は libuv(裏方)に任せて次々リクエストを捌く!
  2. メインのブロックは厳禁: 1人を大計算で拘束すると、サーバー全体がフリーズして大事故になる!
  3. フェーズと割り込み: イベントループの各フェーズを回りながら処理する。ただし Promise などのマイクロタスクは常に最優先で割り込む

次回:第5回「package.jsonの神髄を解き明かす & モジュールシステム混迷の歴史とモダン化への道」に続く

タイトルとURLをコピーしました