ブロッキングとノンブロッキングの概要
この概要では、Node.js におけるブロッキングとノンブロッキング呼び出しの違いについて説明します。 この概要ではイベントループと libuv について説明しますが、 これらのトピックに関する事前知識は必要ありません。 読者は JavaScript 言語と Node.js コールバックパターンの基本的な知識を持っていることを前提としています。
"I/O" とは、主に libuv がサポートしている システムのディスクやネットワークとのやり取りを指します。
ブロッキング
ブロッキングは、Node.js プロセス内の追加の JavaScript の実行が、 JavaScript 以外の操作が完了するまで待たなければならない場合です。 これは、ブロッキング操作が行われている間は イベントループが JavaScript の実行を継続できないために起こります。
Node.js では、 I/O などの JavaScript 以外の操作を待機するのではなく、CPU に負荷がかかるためパフォーマンスが低下する JavaScript は通常、 ブロッキングとしては呼び出されません。 Node.js 標準ライブラリの libuv を使用する同期メソッドは、最も一般的に使用されているブロッキング操作です。 ネイティブモジュールにはブロッキングメソッドもあります。
Node.js 標準ライブラリのすべての I/O メソッドは非同期バージョンを提供します。
これらはノンブロッキングで、コールバック関数を受け入れます。
一部のメソッドにはブロッキングに対応したものもあり、
その名前は Sync
で終わります。
コードを比較する
ブロッキングメソッドは同期的に実行され、 ノンブロッキングメソッドは非同期的に実行されます。
例としてファイルシステムモジュールを使用する場合、これは同期的なファイルの読み取りです:
const fs = require('fs');
const data = fs.readFileSync('/file.md'); // ファイルが読み込まれるまでここでブロック
そして、これは同等の非同期的なコードの例です:
const fs = require('fs');
fs.readFile('/file.md', (err, data) => {
if (err) throw err;
});
最初の例は2番目の例よりも単純に見えますが、 2行目がファイル全体が読み取られるまで追加の JavaScript の実行をブロックするという欠点があります。 同期バージョンでは、エラーがスローされた場合はそれをキャッチする必要があるか、 プロセスがクラッシュします。 非同期バージョンでは、 示されているようにエラーをスローするかどうかを決めるのは開発者次第です。
例を少しだけ拡張しましょう:
const fs = require('fs');
const data = fs.readFileSync('/file.md'); // ファイルが読み込まれるまでここでブロック
console.log(data);
moreWork(); // console.log の後に実行されます
そして、これは似ていますが、同等ではない非同期の例です。
const fs = require('fs');
fs.readFile('/file.md', (err, data) => {
if (err) throw err;
console.log(data);
});
moreWork(); // console.log の前に実行されます
上記の最初の例では、console.log
が moreWork()
の前に呼び出されます。
2番目の例では、fs.readFile()
はノンブロッキングであるため、JavaScript の実行は続行でき、
moreWork()
が最初に呼び出されます。
ファイルの読み込みが完了するのを待たずに moreWork()
を実行する機能は、
より高いスループットを可能にする重要な設計上の選択です。
並行性とスループット
Node.js での JavaScript の実行はシングルスレッドであるため、 同時実行性とは、他の作業が完了した後に JavaScript コールバック関数を実行するイベントループの能力のことです。 同時に実行されることが予想されるコードでは、 I/O などの JavaScript 以外の操作が発生しても、 イベントループの実行を継続できる必要があります。
例として、Web サーバへの各リクエストが完了するのに 50 ミリ秒かかり、 その 50 ミリ秒のうち 45 ミリ秒が非同期に実行できるデータベース入出力である場合を考えてみましょう。 ノンブロッキングの非同期操作を選択すると、 他のリクエストを処理するために 1 リクエストあたり 45 ミリ秒が解放されます。 これは、ブロッキングメソッドの代わりにノンブロッキングメソッドを使用することを選択しただけで、 キャパシティが大きく異なることを意味します。
イベントループは、 並行作業を処理するために追加のスレッドが作成される可能性がある他の多くの言語のモデルとは異なります。
ブロッキングコードとノンブロッキングコードが混在する危険性
I/O を扱うときに避けるべきいくつかのパターンがあります。 例を見てみましょう。
const fs = require('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('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()
へのノンブロッキング呼び出しを行います。
これにより、正しい操作順序が保証されます。