Node.jsでノンブロッキングなコードを書くべき理由

f:id:k-kty:20190111210413p:plain
Node.jsのロゴ

Node.jsで「イベントループをブロックするコードを書いてはだめで、ノンブロッキングなコードを書かないといけない」とかよく言われます。Node.jsを用いて実際に開発をしている人にとっては至極当然のことですが、なぜノンブロッキングなコードを書かないといけないのか、というのをもう一度まとめてみます。

ブロッキングなコードの例

fsモジュールのreadFileSyncはブロッキングな関数の最たる例です。

const fs = require('fs');
const data = fs.readFileSync('/path/to/file');
// dataを使った操作

とても理解しやすいのですが、「こういうことはやってはいけません」とよく言われます。

ノンブロッキングなコードの例

fs.readFileはfs.readFileSyncのノンブロッキング版です(fs.readFileSyncがfs.readFileのブロッキング版と言ったほうがいいのかもしれないですが...)。

const fs = require('fs');
fs.readFile('/path/to/file', (err, data) => {
  // dataを用いた操作
});

なぜだめなのか

Node.jsにおいて、ユーザー(開発者)が書いたJavaScriptの処理は基本的に一つのスレッド(メインスレッド)の中で処理されます。メインスレッドではイベントループが走っています。

基本的に、一つのスレッドの中では同時に一つの処理(関数)しか実行できません。そのため、fs.readFileSync() の実行中(ファイル読み込み待ち中)は、他の処理が何もできません。例えばこのコードがサーバーアプリケーション内にあったとしたら、fs.readFileSync() の実行中はレスポンスを返すことができないのです。

fs.readFile() の場合はそうではありません。それ自体の関数の実行は一瞬で終わり、Node.js(に組み込まれているlibuv)がファイル読み込みのためのスレッドを立ち上げ、その処理が完了した時点(正確には少し後)でコールバック ((err, data) => { ... }) がメインスレッドで実行されます。よって、メインスレッドはファイルの読み込み待ちの間も他の処理をすることができます。

注: ファイルのI/Oは上のようにメインスレッド以外のスレッドで処理をするのですが、ネットワークI/Oの場合は他のスレッドを用いない処理も多いです。

ブロッキングなコードを書いてもいいとき

fs.readFileSync() が存在するくらいなので、ブロッキングなコードを書いてもいいときは存在します。たとえサーバーアプリケーションだとしてもです。

例えば、プログラムの起動時に config.txt を読み込んで、module.exports を用いて他ファイルから利用(require)したいとします。

そういったときにこういったことはできません。

fs.readFile('./config.txt', (err, data) => {
 // 何らかの処理
 module.expoorts = ...;
});

この問題は fs.readFileSync() などのブロッキングな操作を用いれば解決します。こういった場合が、ブロッキングなコードを書いてよい場合です。

おまけ

const f = (n) => {
  let ret = 0;
  for (let i = 0; i < n; i++) ret += 1;
  return ret;
};

もちろん、このコードはブロッキングな関数です。f(1000000000) みたいなことをするとイベントループはブロックされてしまいます。こういうときは(workerを使うか、C++のアドオンを使うかなどして)別スレッドを立てて処理するようにしないといけません。