ブロックとノンブロッキングの概要
この概要では、Node.js におけるブロッキングとノンブロッキング呼び出しの違いについて説明します。この概要ではイベントループと libuv について言及しますが、それらのトピックに関する事前の知識は必要ありません。読者は、JavaScript 言語と Node.js のコールバックパターンの基本的な理解があることを前提としています。
INFO
「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') // ここでファイルの読み取りが完了するまでブロックする
そして、これは同等の非同期的な例です。
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') // ここでファイルの読み取りが完了するまでブロックする
console.log(data)
moreWork() // console.logの後で実行される
そして、これは類似していますが、同等ではない非同期的な例です。
const fs = require('node: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 サーバーへの各リクエストの完了に 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()
への非ブロッキング呼び出しを配置することで、操作の正しい順序が保証されます。