ブロッキングとノンブロッキングの概要
この概要では、Node.jsにおける**ブロッキング**呼び出しと**ノンブロッキング**呼び出しの違いについて説明します。この概要ではイベントループとlibuvについて言及しますが、それらのトピックに関する事前知識は必要ありません。読者は、JavaScript言語とNode.jsのコールバックパターンの基本的な理解があることを前提としています。
「I/O」とは、主にlibuvによってサポートされているシステムのディスクとネットワークとのやり取りを指します。
ブロッキング
**ブロッキング**とは、Node.jsプロセス内での追加のJavaScriptの実行が、非JavaScript操作が完了するまで待機しなければならない状態です。これは、**ブロッキング**操作が発生している間、イベントループがJavaScriptの実行を継続できないためです。
Node.jsでは、I/Oなど、非JavaScript操作を待機するのではなく、CPU集約型であるためにパフォーマンスが低下するJavaScriptは、一般的に**ブロッキング**とは見なされません。libuvを使用するNode.js標準ライブラリの同期メソッドは、最も一般的に使用される**ブロッキング**操作です。ネイティブモジュールにも**ブロッキング**メソッドが存在する場合があります。
Node.js標準ライブラリのすべてのI/Oメソッドは、**ノンブロッキング**である非同期バージョンを提供し、コールバック関数を受け入れます。一部のメソッドには、`Sync`で終わる名前を持つ**ブロッキング**対応物もあります。
コードの比較
**ブロッキング**メソッドは**同期的に**、**ノンブロッキング**メソッドは**非同期的に**実行されます。
ファイルシステムモジュールを例として、これは**同期的な**ファイル読み取りです
const fs = require('node:fs');
const data = fs.readFileSync('/file.md'); // blocks here until file is read
そして、同等の**非同期的な**例を示します。
const fs = require('node:fs');
fs.readFile('/file.md', (err, data) => {
if (err) throw err;
});
最初の例は2番目の例よりもシンプルに見えますが、2行目がファイル全体が読み込まれるまで追加のJavaScriptの実行を**ブロッキング**するという欠点があります。同期バージョンでは、エラーがスローされた場合、それをキャッチする必要があるか、プロセスがクラッシュすることに注意してください。非同期バージョンでは、エラーをスローするかどうかは作成者に任されています。
例を少し拡張してみましょう。
const fs = require('node:fs');
const data = fs.readFileSync('/file.md'); // blocks here until file is read
console.log(data);
moreWork(); // will run after console.log
そして、これと似たような、しかし同等ではない非同期的な例を示します。
const fs = require('node:fs');
fs.readFile('/file.md', (err, data) => {
if (err) throw err;
console.log(data);
});
moreWork(); // will run before console.log
上記の最初の例では、`console.log`は`moreWork()`の前に呼び出されます。2番目の例では、`fs.readFile()`は**ノンブロッキング**であるため、JavaScriptの実行を継続でき、`moreWork()`が最初に呼び出されます。ファイル読み取りが完了するのを待たずに`moreWork()`を実行できる能力は、より高いスループットを可能にする重要な設計上の選択です。
並行性とスループット
Node.jsでのJavaScriptの実行はシングルスレッドなので、並行性とは、他の作業を完了した後にイベントループがJavaScriptコールバック関数を実行できる能力を指します。並行的に実行されることが期待されるコードは、I/Oなどの非JavaScript操作が発生している間、イベントループが実行を継続できるようにする必要があります。
例として、ウェブサーバーへの各リクエストの完了に50msかかり、その50msのうち45msが非同期で実行できるデータベースI/Oであるケースを考えてみましょう。非ブロッキングの非同期操作を選択することで、リクエストごとに45msが解放され、他のリクエストを処理できるようになります。ブロッキング方式ではなく非ブロッキング方式を使用するだけで、容量に大きな違いが生じます。
イベントループは、多くの他の言語におけるモデルとは異なり、同時作業を処理するために追加のスレッドを作成する可能性はありません。
ブロッキングコードと非ブロッキングコードを混在させる際の危険性
I/Oを扱う際に避けるべきパターンがいくつかあります。例を見てみましょう。
const fs = require('node:fs');
fs.readFile('/file.md', (err, data) => {
if (err) throw err;
console.log(data);
});
fs.unlinkSync('/file.md');
上記の例では、fs.unlinkSync()
がfs.readFile()
の前に実行される可能性があり、これは実際に読み取る前にfile.md
を削除してしまう可能性があります。これをより適切に記述する方法として、完全に非ブロッキングで、正しい順序で実行されることが保証される方法があります。
const fs = require('node:fs');
fs.readFile('/file.md', (readFileErr, data) => {
if (readFileErr) throw readFileErr;
console.log(data);
fs.unlink('/file.md', unlinkErr => {
if (unlinkErr) throw unlinkErr;
});
});
上記では、fs.readFile()
のコールバック内にfs.unlink()
への非ブロッキング呼び出しを配置することで、操作の正しい順序が保証されます。