【nodejs基礎講座】Node.jsを組込みアプリケーションに使う前に知っておきたい「イベントループ」の仕組み


※ 当ページには【広告/PR】を含む場合があります。
2024/04/23
蛸壺の技術ブログ|Node.jsを組込みアプリケーションに使う前に知っておきたい「イベントループ」の仕組み

Node.jsはもともとはサーバーサイドの動作を念頭にした「イベント駆動型のサーバーアーキテクチャ」ベースの「非同期処理」を実現するアプリケーションになっています。

昨今ではサーバー実機にとどまらず、Raspberry Piのようなシングルボードコンピュータ向けの組込みアプリケーションとしても利用できるようになっています。

通常、C言語等で組込みアプリケーションを作成する場合には、簡単なポーリングの中で、デバイスの初期化や割り込みなどで、一定の時間間隔で処理をくるくる回していくような実装になります。

他方、Node.jsを使って組込みアプリケーションを作成したい場合、Node.js独自の
「イベントループ」を十分考慮しないと、思うようなタイミングで処理が実行されないかもしれません。

今回は組込み開発者目線で、Node.jsのイベントループの話を紹介していきます。


合同会社タコスキングダム|蛸壺の技術ブログ【効果的学習法レポート】nodejsをこれから学びたい人のためのオススメ書籍&教材特集

Node.jsの内部アーキテクチャ〜libuvの仕組み

Node.jsの内部構造を詳しく網羅していくと切がない話なので、今回は「イベントループ」に密接に関連している「libuv」に着目して説明していきます。

合同会社タコスキングダム|蛸壺の技術ブログ

上の図で表すように、Node.jsは様々なOS上で共通のアプリケーションを実行させるための、「クロスプラットホーム」のJavascript実行環境です。

より低レイヤーのOS側には、「V8」や「libuv」の主要なライブラリや、いくつかのOSカーネルに接続するC/C++ライブラリで構成されています。

このうち特に、「libuv」はイベントループを"非同期"に処理するための仕組みを提供しています。

もう一層上の中間レイヤーには、Javascript側からC/C++のライブラリを呼べるように橋渡しする
「Bindings」、Node.js本体のコアモジュール「Standard Library」、ユーザー独自にC/C++でカスタマイズしたモジュールを利用する「C/C++ AddOns」が介在して低レイヤーの関数を適切に利用できるようにしてくれます。

こういったOS側にある低レイヤーの煩雑な仕組みをNode.jsが全て受け持ってくれるおかげて、Nodejsアプリケーションの開発者は、中間レイヤーより上のアプリケーション層(API層)だけに意識しながら、Javascriptコードによるアプリケーション作成に集中することが可能になります。

さて、全ての処理が仮に"同期的"であれば、「イベントループ」という仕組みも不要でそもそもlibuvを使う必要もないのですが、現実は他のサーバーからHTTPS通信などで返されたレスポンスを非同期で受け取る必要がありますし、他にも様々な非同期の処理がサーバー内部で動いています。

Node.jsを組込みで使う場合にも例外ではなく、カメラから写真をキャプチャするのにシャッターボタンを押すときの割り込みは、非同期処理で実現したほうが圧倒的にパフォーマンスが良いです。

libuvとは?

Node.jsでの非同期処理を一手に受け持つのが
「libuv」となります。

Asynchronous I/O made simple. - libuv is a multi-platform support library with a focus on asynchronous

公式ドキュメントの模式図から、libuvの内部構成は以下のようになっています。

合同会社タコスキングダム|蛸壺の技術ブログ

ちなみにlibuvの謳う、「マルチプラットフォームの非同期I/O処理」の部分が、
epoll (Linux)kqueue (BSD)event port (Solaris)IOCP (Windows)といった主要な各OSの非同期I/O処理のライブラリに対応していることも見て取れます。


合同会社タコスキングダム|蛸壺の技術ブログ【効果的学習法レポート】nodejsをこれから学びたい人のためのオススメ書籍&教材特集

Node.jsのイベントループ

一般的に「イベントループ」の意味するところは、非同期I/Oモデルの一つとしてサーバーアーキテクチャに採用されているものです。

文字通り、イベント通知を発生させて、常時監視している仕組みを意味しています。

このイベントループの利点として、クライアントからのサーバーへのコネクションを、シングルスレッドでも十分処理し、かつ他のプロセスは止めない「ノンブロッキングI/O」を実現することが可能となるメリットがあります。

特に、Node.jsのイベントループは「libuv」に基づいたものです。

ちなみに、ブラウザでも"イベントループ"の仕組みが存在しますが、こちらはHTML5に規定されるものであり、Node.jsのイベントループとは別物ですので注意が必要です。

ここではイベントループといえば、「Node.js(=libuv)のイベントループ」という意味合いで以降で説明していきます。

libuvのイベントループ

先程のからの繰り返しになりますが、イベントループとは、
「シングルスレッド」かつ「ノンブロッキングI/O」でJavascriptコードを実行できるようにする仕組みです。

このイベントループを端的に表した模式図が以下のようなものです。

合同会社タコスキングダム|蛸壺の技術ブログ

Node.jsアプリケーションを起動すると、イベントループが初期化され、同期的な処理(通常のタスク)が逐次コールスタックに追加されて実行、ついで6つのフェイズと2つのマイクロキューが順次実行されていきます。

ちなみにここでのイベントループ開始前の初期化処理とは、

            
            1. タイマーがあればスケジュールを設定
2. process.nextTick関数等のマイクロキューの残りを実行
3. 同期タスクがあれば実行
4. 非同期APIのコールバック呼び出し
        
といったことを指します。

イベントループ初期化前の挙動や、通常の同期処理はさておき、動作中のイベントループを理解するためには、以下の6つのフェイズの役割が重要です。

順序

フェイズ名

イベントキュー名

概要

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系イベントのコールバックを実行

この6つの各フェーズにそれぞれ実行するコールバックのジョブキュー(=イベントキュー)が存在し、

            
            1. キューにあるジョブを全て実行する(=Emptyになる)
2. キューにあるコールバックの数が最大数(上限)に達する
        
のどちらかの条件が満たされると次のフェイズに移行します。

またこの6つのフェイズとは別に、各フェイズ内で発生した
「マイクロタスク」を処理させるための「マイクロタスクキュー」と呼ばれる仕組みも存在します。

マイクロタスクの役割は、各フェイズでコールバックの処理が終了した後で、その結果を一度Nodejsアプリ側へ伝達させるためのものです。

このマイクロタスクキューには、
「nextTickQueue」「microTaskQueue」という2つのキューが存在します。

この2つキューはlibuvではなく、Node.js側から提供される機能となっています。

順序

マイクロタスク

キュー名

概要

1

nextTick

nextTickQueue

Promise系の非同期処理よりも先行して処理を実行

2

microTask

microTaskQueue

PromiseなどJavascriptビルドインの非同期処理を実行

そのため、イベントループからは切り離されたNode.jsに属するキューであり、イベントループの外側に存在する非同期のAPIという扱いに注意が必要です。

イベントループの各フェーズの後にマイクロタスクキューに送られたコールバックが処理され、全てキューが空になるまで実行します。

合同会社タコスキングダム|蛸壺の技術ブログ【効果的学習法レポート】nodejsをこれから学びたい人のためのオススメ書籍&教材特集

リアクターパターン

先程、各フェイズの「イベントキュー」に、「コールバックを送る」と簡易的な表現に留めていましたが、実際にイベントキューへハンドラ(コールバック)を送る仕組みは少し複雑です。

Node.jsデザインパターン 第2版
』を参考にすると、Node.jsの採用する「イベント駆動」の設計モデルは、「リアクターパターン」と呼ばれています。

実際のNode.js内部で用いられているリアクターパターン以外に、スレッドプールによる非同期処理の仕組みも上手く競合させているため、もっと複雑な仕様のようですが、リアクターパターンだけに着目すると以下のようなものに簡略的に模式化されます。

合同会社タコスキングダム|蛸壺の技術ブログ

この図では、処理をイベントループの時間軸に並べて、処理のおおよその流れを説明したものです。

イベントループ中に、まずアプリケーション側から
「イベント・デマルチプレクサ」に対してI/Oリクエストが発行され、監視対象となるハンドラとリソース、オペレーションなどが登録されます(①)。

このとき、I/Oリクエストの発行自体はメインスレッドの処理を止めない(ノンブロッキング)ため、即座にアプリケーションへ処理が戻ります。

イベント・デマルチプレクサは、 I/Oリクエストを受け取った後で、対象のフェイズのイベントキューへ、イベントとハンドラを更に発行します(②)。

イベントループは、各フェイズに遷移したタイミングで、対象のイベントキューの中身に従って、実行すべきイベントを送り出します(③)。

各フェイズで実行予定のイベントに対して、紐付けされたハンドラが呼び出され、ここでメインスレッドで実行に移されます。

選択されたハンドラの処理が順次一つ一つ捌かれるので、一つのハンドラ処理が終わると、また次のハンドラが処理され、イベントキューが順次消化されるのを繰り返します(⑤a)。

もしくは、処理中のハンドラから更にI/Oリクエストが発行された場合には、イベントループ処理へ戻す前にアプリケーション側からI/Oリクエストを繰り返します(⑤b)。

対象のフェイズの全てのイベントキューが処理されると、新しいイベントがイベントキューに入るまで待機しています(⑥)。

というのが、リアクターパターンの基本思想のようです。

ちなみに、イベント・デマルチプレクサにはOSに備わっている非同期I/Oイベントを管理・監視する仕組みがあり、この機能は各OSごとに異なるようですが、このへんをlibuvが上手く処理してくれています。


合同会社タコスキングダム|蛸壺の技術ブログ【効果的学習法レポート】nodejsをこれから学びたい人のためのオススメ書籍&教材特集

実際のJavascriptコードでイベントループの処理を確認する

先程までで、イベントループの概要をおおむね説明してきました。

ここからは"百聞は一見に如かず"ということで、実際のコードを動かして実行順序を確認してみます。

手元の動作確認した環境では、以下のnodejsのバージョンにしています。

            
            $ node --version
v20.12.2
        
ESModlue対応を意識したコーディングになっているため、Requireでモジュールを読み込む古いnode(CommonJS)では以下のコードが動作しないかもしれませんのでご容赦ください。

setTimeout関数を利用する際の注意点

Node.jsでの非同期処理の起点とも言えるのが、
setTimeout関数であり、第二引数で指定したミリ秒だけコールバックの処理を遅延させることができるものです。

ただ、
setTimeoutの第二引数を指定しなかったり、1 - 2147483647[ms]の範囲外の無効な整数を指定した場合、強制的に1[ms]と見なされます。

ということで、以下は全て1ms(とちょっと)の時間で遅延します。

            
            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});
        
また、マシーンの演算処理のスペックにもよりますが、Timerフェイズが1ms程度の遅延だと、1ms以下の時間でTimerキューに入ったコールバックを消費されてしまい、キューが空になったとイベントループに判断されると、次のフェイズに送られ、そのラウンドが終了してしまう可能性もあります。

これを以下のコードで確認してみましょう。

            
            //👇ちょっと長め(=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です
        
となったり、実行するたびに異なる間欠的な実行結果になります。

タイマーを1msで使う機会はあまりないので、普段は気に留める必要もないのですが、各フェイズのイベントキューが「空になった」・「次のフェイズ進む」のは実行中のNodejsプロセスに全てお任せであり、開発者が手動でフェイズ遷移のタイミングを決定することはできないことにも注意です。

同期処理・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です
        
では、メインスレッドにある同期処理(通常の関数)と、マイクロタスク(nextTickPromise)は一旦全て実行されます。

これらの処理が完了すると、イベントループがTimerフェイズから開始されます。

            
            Timerフェイズ1です
マイクロタスク3です
マイクロタスク4です
Pendingフェイズ1です
マイクロタスク7です
マイクロタスク8です
Checkフェイズ1です
マイクロタスク5です
マイクロタスク6です
        
までが、イベントループの1ラウンド目で、Timer --> Pending --> Checkの順でフェイズが遷移されているのが分かります。

また各フェイズの終わりには、フェイズ内部で発行されたマイクロタスクが全て実行されていることも見て取れます。

なお、
fs.readFileの部分であえて存在しないファイルを読み込みこんでエラーを出しています。

ここでI/Oエラーが出さないと、Pendingフェイズの処理結果が変わり、2ラウンド目の処理が変化します。

詳しくは後述するPendingフェイズのパートで解説します。

ここでの次のイベントループ2ラウンド目では、

            
            Timerフェイズ2です
マイクロタスク9です
マイクロタスク10です
Pendingフェイズ2です
マイクロタスク11です
マイクロタスク12です
Closeフェイズ1です
マイクロタスク13です
マイクロタスク14です
        
となっています。

1ラウンド目のPendingフェイズ内で更に発行した新たなTimerイベントがTimerキューに送りこまれているため、イベントループ2週目の始まりのタイミングでsetTimeoutのコールバックが実行されています。

また1ラウンド目では実行されなず持ち越しになっていた
fs.createReadStreamの非同期I/O処理も2ラウンド目のPendingフェイズで処理されています。

このラウンドでClose Callbackキューに入れられたハンドラも実行されていることが確認できます。

ということで、2ラウンド目は、
Timer --> Pending --> Closeの順で実行されていることも分かります。

Pollフェイズの確認

最後に6つのイベントフェイズの中で、理解のしにくさ故に、先程のコードからわざと除外していた、
Idle/PrepareフェイズとPollフェイズについて考えてみます。

まず、
Idle/Prepareフェイズはlibuvの内部処理専用に呼び出されるフェーズですので、Node.jsアプリ開発者側からは触ることができません。

Idle/Prepareフェイズの主な役割は、次のフェーズであるPollフェイズの事前処理などを担当しています。

Pollフェイズもlibuvが内々に処理を行うためのフェイズで、サーバからの応答、ストレージからのファイル読み込み、新しい通信ソケットの確立といった、まだ結果の返されていない処理途中のI/Oイベントを待機するために使用される特殊なフェイズです。

ネットワーク接続やディスクアクセスなど、I/Oイベントに基づくコールバックのほとんどがこのフェイズで実行されます。

まずPollフェイズでは、ポーリングする時間を計算します。

このポーリング時間は、状況によって計算結果が変わるようです。

Nodejs側から要求されたI/Oイベントを各OS側のジョブキューに全てへ送り出します。

OSカーネルのシステムコールを呼び、計算された時間でポーリングを行い、カーネルから届くIOイベントを待ちます。

ポーリングで待っている間にI/O処理が完了したら、そのままPollキューに入ったコールバックが実行されます。

Pollフェイズも基本的にPollキューが空になるか、キュー数の上限に達するなどで次のCheckフェイズへ遷移します。

ただしとりわけ注意が必要なのが、他のフェイズとは違うPollフェイズの特殊ルールが存在します。

            
            1. TimersキューかCheckキューが空でない(スケジューリングされている)場合、
    Pollフェーズを一旦終了し、次のCheckフェーズへ進む
2. TimersキューとCheckキューが空になっている(スケジューリングされていない)場合、
    Pollフェイズでキューが空になるまで待ち続ける
        
というようにPollフェイズは、TimersフェイズとCheckフェイズに常に気にかけながら動いている、「上と下が気になってしょうがない」フェイズと言えます。

先程の例の中で、故意に存在しないファイルを読み込ませエラーレスポンスを使うことで、Pollフェイズの状態に変化を与えていたところだけを使って、このPollフェイズの特殊ルールを確認してみましょう。

先程のサンプルコードに少し手を加えて、以下のようなコードでPollフェイズの挙動を調べてみます。

まずは先行するファイルI/O処理でエラーレスポンスが返るサンプルコードです。

            
            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です
        
比較で、ファイルI/O処理が両方とも正常に完了する場合をやってみます。

            
            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です
        
となり、イベントループの実行順序がガラッと異なります。

なぜI/O処理のレスポンス結果が違うだけで、実行されるイベントループのラウンドが異なってしまうかを考慮するには、
Pollフェイズの特殊ルールを考慮する必要があります。

イベントループ開始時に2つの
fs.readFileのコールバックがPollキューに登録されてから、Pollフェイズ中で、ファイルI/OイベントがOSカーネル側へ送られて、その応答をPollキューが空になるまで待機しているわけですが、2つのI/Oイベントとも正常なレスポンスが返った場合、そのレスポンスが返される時間間隔は非常に短い時間で完了します。

つまり、1つ目の
fs.readFileの中に仕込まれたsetTimeoutからのタイマーがタイムアウトするよりも早くPollキューが空になってしまうので、イベントループの1ラウンド目のPendingフェイズで両方まとめてコールバック処理が完了することになります。

ということで、イベントループの2ラウンド目は、遅延して発行されたタイマーのコールバックだけが「
Timerフェイズ2です」で実行されるだけになっています。

ところが、1つ目の
fs.readFileがOS側からエラーレスポンスを受け取ってしまった場合、正常なレスポンスの時と比べて、若干処理に時間をとられてしまうようになります。

すると微妙な時間差ですが、その内部で発行していた
setTimeoutタイマーのほうが先にタイムアウトしてしまうため、Timersキューにコールバックが登録されてしまいます。

2つ目の
fs.readFileのI/O処理のレスポンスが返る前に、Timersキューがスケジュールされて(空ではなくなって)しまったと判断し、イベントループはPollフェイズの処理を中断し、次のCheckフェイズへ移行することになります。

ということで、2つ目の
fs.readFileのコールバック処理は、イベントループの2ラウンド目で実行されている、という流れになっています。

...Pollフェイズはなかなかに複雑な仕組みですが、I/O処理の実行ラウンドを厳密に管理するようなNodejsプログラムを開発するケースはかなり稀だと思います。

さほどシビアに捉えず、Node側にタイミングを全てお任せする余裕を持たせたアプリケーション設計を心がけるほうが良いでしょう。


合同会社タコスキングダム|蛸壺の技術ブログ【効果的学習法レポート】nodejsをこれから学びたい人のためのオススメ書籍&教材特集

まとめ

今回は、Nodejsアプリケーション開発者ならじっくり知っておきたい、「イベントループ」の仕組みで、特に組込み向けのアプリケーションを作成するなら事前に知っておきたい内容を中心に深堀しておきました。

更にNode.jsの非同期処理の内部構造やベース技術まで詳しく知りたい方は、『
Node.jsデザインパターン 第2版
』を一読されると良いでしょう。