kkty’s blog

おそらく大学3年生です

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++のアドオンを使うかなどして)別スレッドを立てて処理するようにしないといけない。