ブロッキング vs 非ブロッキングの概要

この概要では、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  = ('node:fs');

const  = .('/file.md'); // blocks here until file is read

そして、こちらが同等の非同期の例です

const  = ('node:fs');

.('/file.md', (, ) => {
  if () {
    throw ;
  }
});

最初の例は2番目の例よりもシンプルに見えますが、2行目がファイル全体の読み込みが終わるまで追加の JavaScript の実行をブロックするという欠点があります。同期バージョンでは、エラーがスローされた場合にそれをキャッチしないとプロセスがクラッシュすることに注意してください。非同期バージョンでは、示されているようにエラーをスローするかどうかは作者の判断に委ねられます。

例を少し拡張してみましょう

const  = ('node:fs');

const  = .('/file.md'); // blocks here until file is read
.();
moreWork(); // will run after console.log

そして、こちらが似ていますが同等ではない非同期の例です

const  = ('node:fs');

.('/file.md', (, ) => {
  if () {
    throw ;
  }

  .();
});
moreWork(); // will run before console.log

最初の例では、console.logmoreWork() の前に呼び出されます。2番目の例では、fs.readFile()非ブロッキングなので、JavaScript の実行を継続でき、moreWork() が先に呼び出されます。ファイルの読み込みを待たずに moreWork() を実行できる能力は、より高いスループットを可能にするための重要な設計上の選択です。

並行性とスループット

Node.js における JavaScript の実行はシングルスレッドなので、並行性とは、他の作業を完了した後に JavaScript のコールバック関数を実行するイベントループの能力を指します。並行して実行されることが期待されるコードは、I/O のような JavaScript 以外の処理が発生している間、イベントループが実行を継続できるようにする必要があります。

例として、Web サーバーへの各リクエストの完了に50msかかり、そのうちの45msが非同期で実行できるデータベース I/O であるケースを考えてみましょう。非ブロッキングの非同期処理を選択することで、リクエストごとにその45msが解放され、他のリクエストを処理できるようになります。これは、ブロッキングメソッドの代わりに非ブロッキングメソッドを使用するだけで、処理能力に大きな違いが生まれることを示しています。

イベントループは、並行処理のために追加のスレッドが作成されることがある他の多くの言語のモデルとは異なります。

ブロッキングコードと非ブロッキングコードを混在させる危険性

I/O を扱う際には避けるべきパターンがいくつかあります。例を見てみましょう

const  = ('node:fs');

.('/file.md', (, ) => {
  if () {
    throw ;
  }

  .();
});
.('/file.md');

上記の例では、fs.unlinkSync()fs.readFile() の前に実行される可能性が高く、実際にファイルが読み込まれる前に file.md が削除されてしまいます。これをより良く書く方法は、完全に非ブロッキングで、正しい順序で実行されることが保証される以下の方法です

const  = ('node:fs');

.('/file.md', (, ) => {
  if () {
    throw ;
  }

  .();

  .('/file.md',  => {
    if () {
      throw ;
    }
  });
});

上記では、非ブロッキングfs.unlink() の呼び出しを fs.readFile() のコールバック内に配置することで、正しい操作順序を保証しています。

参考資料