Node.jsの役に立つお話(主にノンブロッキングIO, 非同期処理)

これはKCS AdventCalendar2019 17日目の記事です.

はじめに

こんにちは, yapattaです.

前回は限界感ある給湯器の記事を晒してしまいましたが, 今回はちゃんと技術的な記事を書きたいと思います!!

せっかく記事を書くならたくさんの人に読んでもらいたいわけで, 皆何かしらの理由で用いるであろうNode.jsのお話です.
Node.jsは他の言語と特性が違くて理解しづらいみたいな話を, 周りからちょいちょい聞きます.
今回はそんな人をターゲットにしたNode.jsが他と違うこと(主にノンブロッキングIO)について知る記事です. 広く浅く扱うので興味を持った方は適当な詳しい記事を読んで下さい.

Agendaは,

  1. Node.jsとは
  2. シングルプロセスとシングルスレッドのお話
  3. ノンブロッキングIOのお話
  4. コールバックのお話
  5. Promiseのお話
  6. Generatorのお話
  7. async, awaitのお話

についてですー

Node.jsとは

まずNode.jsとは何かについて
公式サイトを見ると,

Node.js はスケーラブルなネットワークアプリケーションを構築するために設計された非同期型のイベント駆動の JavaScript 環境です。

みたいなことが書かれています. よくわからないですね.
これは, サーバーサイドで使えるJavaScript環境みたいな感じだと思ってもらえるといい気がします.

でも, サーバーサイドで使うには少し注意が必要です.
これから, Node.jsのちょっと特殊だなーと思うところについて話していきます.

シングルプロセスとシングルスレッドのお話

WebサーバーにもなりうるNode.jsはシングルプロセス, シングルスレッドで動きます.
これは一つのプロセスと一つのスレッドで動くという意味です(文字そのまま).
全体的なリソース消費量が少ないとか, プログラムの読み込みが最小限で抑えられるとか, いろんなメリットがある気がします.
しかし, マシンパワーを最大限使いたいとかたくさんのIOとかをさばきたい人もいるわけで, そんな人のためにクラスタリングという手法があります. 脱線しそうなのでその話はのちのちに…

では次にノンブロッキングIOの話をしたいと思います.

ノンブロッキングIOのお話

ノンブロッキングIOの前に, 大まかにイベントの処理についての話をしていきます.

まずNode.jsでは, イベント(Socketとかファイルの読み込みみたいなIOリクエスト)が発生すると即座に実行されるわけではなくイベントキューというものにとりあえず突っ込まれます. そして, イベントループでイベントが検出されてそのイベントが実行されます. イベントが完了したらそのイベントのコールバックが実行されます. コールバックというのは, イベントが終了後に処理されるものといった感じでしょうか. 金魚のフンみたいな感じです.

まあこんな感じでイベント処理が実行されるわけなのですが注意が必要です. というのもイベントが発生した後, IO処理が完了しなくてもアプリケーション側に処理が戻ってしまうのです. 実際にコードを見てみましょうか.

これはファイルを読みこんだ後にファイルの中身を出力するというコードですが,

と出力されてしまいます.

なんだこれは!こんなんじゃIO使えねえじゃねえか!と思う人もきっといると思います. 僕も最初Node.jsの特性に苦しめられました. しかし, こうすることにはそれなりに理由があるんですねえ.

じゃあ次はその理由について見てきましょう.

まず, FileIOやNetworkIOというのは処理が遅いです. また, データを外部から取り出すためデータを取得するための待ち時間もどうしてもかかってしまいます. これでは後続の処理も遅れてしまうためシングルスレッドで処理するNode.jsにとっては致命的です.

そんなわけで, ブロッキングではなくノンブロッキングIOを採用しているわけです.
ある一定時間でできる処理をできる限り増やしたいぜ!みたいな意向が伝わってきますね.

しかし, IO処理が終了する前に他の処理が実行されて欲しくない場面もあるわけでそんなときにコールバック関数というのを用います.

では次にコールバック関数について話したいと思います.

コールバックのお話

先程話したとおり, あるイベントが発生した後の処理のことです. まあ実際にはIOが完了するとコールバック処理がキューに入り, イベントループがそのキューからコールバック処理を取り出して実行するみたいですが, 簡略化のためコールバック処理はあるイベントが発生した後に行われる処理とみなします.

実際にコードを見るのが早いと思うので, 以下を見て下さい.

今回fs.readFileの第3引数に書く関数がコールバック関数というのですが, 実行結果からわかる通りファイルを読み込んでからファイルの内容が出力されていることがわかりますね.

こんな感じでコールバック関数を用いると, イベント後の処理も安心して書けるのですが注意が必要です.
というのも, イベントが複数実行されたとき入れ子がエグいことになります. 以下の例を見てみましょう. 3つのファイルを読み込んで出力させるだけなのに入れ子が深いせいで, コードがすごくわかりにくく見えてしまいます.

気持ち悪いですね. こんなコードを書きたくないと思ってしまいます.

しかし, 世の中とはうまくできているもので解決策があるんですね.

Promiseのお話

その名もPromiseです!!(でーん!!). ES2015から使えるようになりました.

イベント終了後のコールバック処理をthenに書くことでネストが深くならずにすみます. 実際にコードを見てみましょうか. 0.1秒ごとに1を足して出力するプログラムです. incrementは0.1秒経過したらresolveというコールバック関数を実行する関数です. コールバック関数の内容はthenの後に記述します. 実際に, 0.1秒ごとに1増加していくのがわかると思います.

まあこれでも少し読みづらいですが, コールバック地獄から解放されて大分コードが読みやすくなりました. 素晴らしい.

察しのいい読者は, どうせ次はasyncとawaitについての説明をするんだろ, と思うところでしょう. 実際僕もasyncとawaitのお話をするべきなのでしょうが, どうしてかセオリー通りに行きたくないものでGeneratorの話をします(え??).

Generatorのお話

Generatorの話をするにはもちろん理由がありますよ??
async, awaitの前に追加された機能ですし, Redux-Sagaで使われていますし(元々Redux-Sagaについての記事を書きたかった).

とりあえず実際にコードを見てみましょう. さっきよりも大分コードが読みやすくなりましたね. 非同期処理をやっている実感がわかないほどに.

実際にやっていることはPromiseのコードと同じになります. 0.1秒ごとに1を足しています.
yieldと書いた処理が実行されるまで次の処理が実行されなくなっているのがわかると思います. 0.1秒経ったらnumに1足された値がnumに代入されることがわかりますね.

Async, Awaitのお話

なんだかんだでasync, awaitのお話は需要があると思うので.

先程のcoにすごく似ています. async関数はawaitを含むことができ, awaitと書いた処理が実行されるまで次の処理が実行されなくなります.

まとめ

Node.jsのお話を長々としました.
Node.jsのノンブロッキングIOという特殊な性質に興味を持っていただければ幸いです.

今回は, クラスタリングとか, Node.jsがシングルスレッドで動くようになった時代背景について詳しく話せなかった(記事がさらに長くなると考えると恐ろしい…)ので, 今度時間があるときにまた記事を書きたいなーと思っております.

では, よいお年をー!!

Posted on