• トップ
  • ブログ一覧
  • 【Node.js】イベントループの概要について
  • 【Node.js】イベントループの概要について

    広告メディア事業部広告メディア事業部
    2020.07.02

    IT技術

    Node.jsとは?

    「Node.js」をひと言で表すと、「サーバサイドのJavaScriptプラットフォーム」です。

    JavaScript ですので、クライアントサイドでのノウハウが活用できます。

    クライアントサイドでの JavaScript の経験があれば取っつきやすい反面、他の言語と比較した際の違いに戸惑う人がいるかもしれません。

    ここでは、「Node.js」の特徴のひとつであるイベントループの概要について説明します。

    Node.jsのシングルスレッドの動作を確認

    まずは簡単なコードを動かして、「Node.js がシングルスレッドで動作する」ということを実感しましょう。

    ここでは、「http モジュール」のみでリクエストを処理してみます。

    アクセス回数を表示するサンプル

    '/' にリクエストを受けた回数を画面に返すだけのサンプルを示します。

    グローバル変数 count によってアクセスの回数を記録します。

    1const http = require('http'); // httpモジュールをrequire
    2const PORT = 3000; // 待機するポートは3000
    3let count = 0; // アクセスされた回数
    4
    5const server = http.createServer(); // Serverオブジェクトを作成
    6server.on('request', doReq); // requestイベントにdoReqを登録
    7server.listen(PORT); // 待機
    8
    9// requestイベント発火時時に実行する関数
    10function doReq(req, res) {
    11  if(req.url == '/') count++;
    12  res.end(`count:${count}`); // アクセスされた回数をクライアントに返す
    13}

    実行!

    書いたら実行し、ブラウザでアクセスしてみましょう。

    コードを「index.js」として保存したのであれば、次のコマンドで実行できます。

    1$ node index.js

    実行結果

    http://localhost:3000/ にブラウザでアクセスすると、画面には次のようにアクセス回数が表示されることでしょう。

    count:1

    何度かアクセスすると、そのたびに数字が「2, 3, 4」と増えていきます。

    ブラウザを変えてアクセスしても、数字は「5, 6, 7」と増えていきます。

    結果からわかること

    アクセスのたびに増える変数 count は、スクリプトを停止させない限り、メモリ上に保持され続けています。

    この挙動から、Node.js がシングルスレッドによって動作していることが確認できます。

    7行目の server.listen(PORT); からも見て取れるように、このスクリプト自体がリクエストを受け付けています。

    「何者かがリクエストに応じてスクリプトを実行する」というものではありません。

    なぜシングルスレッドで処理できるのか

    Node.js はシングルスレッドだということが確認できました。

    シングルスレッドではアクセスのたびにスレッドが増えないため、メモリ消費量を少なく抑えられるなどのメリットがあります。

    ノンブロッキングI/O

    ここで、なぜ Node.js がシングルスレッドで多数のアクセスを処理できるのかというと、ノンブロッキング I/O を採用しているためです。

    「ネットワークとのやりとり」、「データベースアクセス」、「ファイル入出力」など、時間のかかる処理を他のプロセスに任せ、その間に Node.js 自身ができる処理を進めます。

    I/O イベントを取得したタイミングで、「次はこれを実行すること」という形でキューにコールバックを追加し、順次 Node.js が処理します。

    サンプルで言うと

    先に示したアクセス回数を表示するサンプルで言えば、次の記述です。

    1server.on('request', doReq); // requestイベントにdoReqを登録

    リクエストのイベントを取得したら、doReq 関数をコールバックとして登録します。

    つまり

    仮にリクエストが巨大でも、Node.js はその終了を待たずに他の処理を継続することができます。

    Node.js は、そのようにして処理を他のプロセスに任せることで効率的に動作しているのです。

    Node.jsのイベントループの概要

    Node.js は、「何かが起こったら、こういう順番で処理を進める」ということを繰り返しています。

    これが、 Node.js の「イベントループ」です。

    概要図

    簡単な図にすると、次のようになります。

    ざっくり言えば、「I/O が発生したら一連の処理を実行し、次の I/O を待つ」ということになります。

    Node.js は、このループを延々と繰り返します。

    詳しい説明

    より詳しい説明は nodejs.org にありますので、興味のある方は参照してください。

    【nodejs.org:Node.js コアコンセプト】
    https://nodejs.org/ja/docs/guides/event-loop-timers-and-nexttick/

    同期処理によるイベントループの停滞

    I/O が発生した際、Node.js は実行すべき処理を次々とキューに追加しますが、処理は停滞しません。

    タイマーによる処理であっても、特定のタイミングで実行するコールバックを登録しておき、先の処理を進めます。

    しかし、実行する処理そのものを同期処理として記述すると、イベントループを停滞させる可能性があります。

    ループをブロックしてしまう例

    先ほどのサンプルで書いた doReq  関数を書き書き替えて、イベントループの停滞(ブロック)について確認してみましょう。

    コード

    1function doReq(req, res) {
    2  if(req.url == '/') {
    3    count++;
    4  }else if(req.url == '/heavy') {
    5    count++;
    6    for(let i = 1; i <= 10000000000; i++) {
    7      // この部分で時間がかかる
    8    }
    9  }
    10  res.end(`count:${count}`);
    11}

    変更内容

    '/heavy' にアクセスがあった場合に、変数 count をインクリメントする以外に、100 億回ループするだけの for 文を追加しました。

    (ちなみに JavaScript における整数型の最大値は 253-1、つまり 9000 兆以上です)。

    '/' にアクセスした際の挙動は先ほどと同じですが、'/heavy' にアクセスした場合はレスポンスが返されるまでに非常に時間がかかります。

    そして'/heavy' がレスポンスを返すまで、Node.js は次のリクエストさえ受け付けません。

    結果確認

    ブラウザで'/heavy' を開き、レスポンスが返ってくる前に別のブラウザで '/' を開いてみましょう。

    '/heavy' の同期処理が終了するまでは、'/'にアクセスした結果も返されません。

    イベントループの概要で示した図の「コールバックの実行」部分で停滞している状態です。

    非同期で処理する

    コールバック内に同期処理を記述することにより、イベントループを停滞させる可能性があることがわかりました。

    したがって、Node.js では時間のかかる処理をなるべく非同期で記述する必要があります。

    ループをブロックさせないために

    先ほどのコードを、今度は非同期で実行されるよう書き換えてみます。

    コード

    1function doReq(req, res) {
    2  if(req.url == '/') {
    3    count++;
    4  }else if(req.url == '/heavy') {
    5    count++;
    6    let i = 1;
    7    // 非同期で実行する
    8    const timeout = setInterval(function(){
    9      if(i == 10000000000) clearInterval(timeout); // 100億回に達したらsetIntervalを終了
    10      i++;
    11    },0);
    12  }
    13  res.end(`count:${count}`);
    14}

    変更内容

    今度は、100 億回の処理を setInterval によって行うようにしました。

    setTimeout と同様に、タイマー処理である setInterval は非同期に実行されます。

    実行結果

    今回のコードを実行すると、100 億回の処理が終了するのを待たずにレスポンスを返します。

    setInterval を利用したことにより、処理が終了する前にレスポンスを返すことができました。

    ただ、見かけ上は軽快に動作するようになりましたが、レスポンスが返される時点で処理が終了している保証はありません。

    処理の終了を待ってレスポンスを返す

    それでは、処理が終了したときにレスポンスを返すように、再度コードを書き替えてみましょう。

    コード

    1function doReq(req, res) {
    2  if(req.url == '/') {
    3    count++;
    4    res.end(`count:${count}`); 
    5  }else if(req.url == '/heavy') {
    6    count++;
    7    let i = 1;
    8    // 非同期で実行する
    9    const timeout = setInterval(function(){
    10      if(i == 10000000000) {
    11        clearInterval(timeout); // 100億回に達したらsetIntervalを終了
    12        res.end(`count:${count}`); // 全てのコールバックを実行したときにレスポンスを返す
    13      }
    14      i++;
    15    },0);
    16  }else{
    17    res.end(`count:${count}`); // それ以外のパスにアクセスがあった場合もレスポンスは返す
    18  }
    19}

    変更内容

    全てのコールバックを実行した時に、レスポンスを返すよう変更しました。

    ただし、 setInterval に登録したコールバックを順次実行するには相当な時間を要します。

    そのため、100億のコールバックの終了を待ってレスポンスを返す場合は恐らくタイムアウトになるでしょう。

    実際に動作を確認する際は、setInterval を終了する条件をゆるく、i が1000に達したくらいで clearInterval するようにして試すのがよいと思います。

    なお、'/heavy' のレスポンスが返される前に '/' にアクセスしても、ちゃんとレスポンスが返るよう変更しています。

    countを増やすタイミング

    '/heavy' のレスポンスが戻ってくるまでの間にも、'/' にアクセスすることは可能です。

    ただし上のコードそのままだと、ようやく '/heavy' からレスポンスが返ってきたときに画面に表示されるのは、「最後に '/' が返した内容と同じもの」になります。

    '/heavy' がレスポンスを返すまでの間に、 '/' へのアクセスによって変数 count の値が更新されてしまうからです。

    「表示するアクセス回数」を「レスポンスが返される順番」と一致させるには、レスポンスを返す直前、res.end(`count:${count}`); の1行前で count を増やすべきでしょう。

    もしも「表示するアクセス回数」を「リクエストを受け付けた順番」と一致させるならば、画面に返す値をローカル変数に格納することになるでしょう。

    いろいろと考えることが多いですね…!

    さいごに

    Node.js のイベントループについて、サンプルを交えて説明しました。

    なるべく簡単に説明したつもりですが、慣れない人には若干ややこしいかもしれません。

    ですが、コツを掴めば Node.js は非常に面白い言語です。

    最初のハードルを越えてしまえば、豊富なライブラリを利用して様々な機能を実現できます。

    ぜひ頑張って習得しましょう!

    次回の記事はこちら

    featureImg2019.12.05【第3回】Vue.js 入門 〜オプション編〜オプションを覚えよう!宮田(株) ライトコード Webエンジニアの宮田です。前回までで、Vue.jsの主要なディレクテ...

    こちらの記事もオススメ!

    featureImg2020.08.07JavaScript 特集知識編JavaScriptを使ってできることをわかりやすく解説!JavaScriptの歴史【紆余曲折を経たプログラミン...

    featureImg2020.07.17ライトコード的「やってみた!」シリーズ「やってみた!」を集めました!(株)ライトコードが今まで作ってきた「やってみた!」記事を集めてみました!※作成日が新し...

    広告メディア事業部

    広告メディア事業部

    おすすめ記事