SlideShare a Scribd company logo
そうだったのか! よくわかる
   process.nextTick()
 Node.jsのイベントループを理解する




   IIJ 大津 繁樹 (@jovi0608)
        2012年6月28日
     東京Node学園6時限目
そうだったのか! よくわかる process.nextTick() node.jsのイベントループを理解する
Nodeの歩み(参考)
2007/10 libev公開           2010/08 nodejs_jp開始
2008/05 libeio公開          2010/09 no.de開始
2009/09 Google V8公開       2010/11 Joyent管轄へ
2009/02 ry Node開発開始       2011/02 node-v0.4.0リリース
2009/05 node-v0.0.1リリース   2011/03 東京Node学園#1
2009/06 nodejs ML開始       2011/10 東京Node学園祭
2009/10 npm公開             2011/11 node-v0.6.0リリース
2009/11 JSConf EU ry発表    2011/12 Azureサポート
2010/04 Herokuサポート        2012/01 isaacs管理へ
2010/08 node-v0.2.0リリース   2012/06 node-v0.8.0リリース
今日の話
•   Nodeのイベントループとは
•   process.nextTickとは
•   node-devでの大論争
•   今後どうなる?
•   process.nextTickの正しい使
    い方

おそらく世界初?の
Node-v0.8ベースでイベント
ループを解説
(libuvの大幅な変更に追随)
(注: 説明はLinuxが対象です。)
Nodeのイベントループとは、

• Node の心臓


  イベントループが終了したら
  Node は死にます。
Nodeのイベントループの正体




  Node が起動する時に uv_run() が呼
  ばれます。(src/node.cc:2910)




https://github.com/joyent/node/blob/v0.8.0-release/deps/uv/src/unix/core.c#L265
イベントループが回り続けるには




                              アクティブな handle/req がなけれ
                                       ば
                                 イベントループが終了

https://github.com/joyent/node/blob/v0.8.0-release/deps/uv/src/unix/core.c#L252-261
handle と req の違い
• handle
  – I/O が発生してない時でもイベントループを
    維持
  – (例) server.listen()



• req
  – I/Oが発生している時だけイベントループを
    維持
  – (例) http.get()
handle と req の種類
              handle                       req
ASYNC        非同期ジョブの操作      CONNECT       stream接続
CHECK        ループの最後の操作      WRITE         stream書き込み
FS_EVENT     ファイルイベント操作     SHUTDOWN      stream停止
FS_POLL      statの問い合わせ操作   UDP_SEND      udp 送信
IDLE         アイドルの時の操作      FS            ファイル操作
NAMED_PIPE   名前付きパイプの操作     WORK          ワーカスレッド
POLL         fdイベントの操作      GETADDRINFO   アドレス情報取得
PREPARE      ループの最初の操作
PROCESS      プロセスの操作
TCP          TCPの操作
                                      後で見て
TIMER        タイマー操作                   おいて下
TTY
UDP
             TTYPの操作
             UDPの操作
                                       さい。
実際のコードでは、(その1)
var http = require('http');         アクティブ
                                     ハンドル
var server = http.createServer();      0




 アクティブハンドルが無いからNode終了
実際のコードでは、(その2)
var http = require('http');
var server = http.createServer();
                                アクティブ
server.listen(1234);           ハンドル追加
                               (+1)




 アクティブハンドルが作成されNode
 は終了しない。実際は epoll wait (Linux)して
実際のコードでは、(その3)
var http = require('http');
var server = http.createServer();
server.listen(1234, function() {
                            アクティブ
 server.close();          ハンドル削除
                             (+1-1=0)
});



アクティブハンドルがすぐ無効化される
のでNode終了
イベントループの中身
  7つのステップ

      1. 時刻更新
      2. タイマー実行
      3. アイドル実行
      4. Prepare実行
      5. I/Oイベント実行
         (libev)
      6. Check実行
      7. ハンドル終了
Node-v0.8イベントループ概要
                             終わり     始まり
                                                    setTimeout()

nextTick()                            1:時刻更新
                7:ハンドル終了

         6:run_check                       2:run_timers

コールバッ
                             イベントループ                 nextTick()
  ク                           一周(Tick)                    ユーザ
             5:poll                          3:run_idle   プログラム


                                   4:run_prepare
        libev+kernel
       epoll: Linux                                  nextTick()
       kqueue: BSD
       event port: Solaris
                              (注: ユーザプログラムは 3: run_idle から始まる
イベントループを止めてはいけない!
                              終わり    始まり
                                                    setTimeout()

 nextTick()                           1:時刻更新
                 7:ハンドル終了

          6:run_check     2:run_timers
                   こんなコードはダメ! while(1)
 コールバッ
   ク                while(1) {
                      console.log(‘hoge’);
             5:poll }                      3:run_idle
                                                  ずっとここ
         libev+kernel                             で止まる!
                                    4:run_prepare
        epoll: Linux
        kqueue: BSD                                  nextTick()
        event port: Solaris
                              (注: ユーザプログラムは 3: run_idle から始まる。
なぜ3カ所も nextTick() があるの?
理由1:呼び出し順番
setTimeout(function(){
  console.log(‘3:foo’);
                                    $ node tick-order.js
}, 0);
                                    1:piyo
process.nextTick(function() {
                                    2:hoge
  console.log(‘2:hoge’);
                                    3:foo
});
console.log(‘1:piyo’);



setTimeout() より process.nextTick() が先に呼ばれる
                            (注: 将来仕様が変わる可能性があります。)
理由1:呼び出し順番
                         終わり     始まり
                                                  setTimeout()
                                               console.log(‘3:foo’)
  nextTick()                      1:時刻更新
                 7:ハンドル終了

           6:run_check                 2:run_timers
                                             console.log(‘1:piyo’)

  コールバッ
                        イベントループ                    nextTick()
    ク                    一周(Tick)
               5:poll                    3:run_idle

                               4:run_prepare
                                               console.log(‘2:hoge’)
                                                    nextTick()

(注: ユーザプログラムは 3: run_idle から始まる。)
理由2:入れ子の呼び出し順番
process.nextTick(function() {
 setTimeout(function(){
    console.log(‘4:foo');
                                  $ node tick-order2.js
 }, 0);
                                  1:piyo
 process.nextTick(function() {
                                  2:bar
    console.log(‘3:hoge');
                                  3:hoge
 });
                                  4:foo
 console.log(‘2:bar');
});
console.log(‘1:piyo’);
process.nextTick() のスコープ内でも setTimeout()
より process.nextTick() が先に呼ばれる
                          (注: 将来仕様が変わる可能性があります。)
理由2:入れ子の呼び出し順番
                            終わり      始まり
console.log(‘3:hoge’)                                 setTimeout()
                                                   console.log(‘4:foo’)
    nextTick()                        1:時刻更新
                        7:ハンドル終了

             6:run_check                   2:run_timers
                                                 console.log(‘1:piyo’)

   コールバッ
                           イベントループ                     nextTick()
     ク                      一周(Tick)
                 5:poll                      3:run_idle

                                   4:run_prepare
                                                   console.log(‘2:bar’)
                                                        nextTick()

(注: ユーザプログラムは3: run_idleから始まる。)
process.nextTick()の説明(マニュアルより)
    イベントループの次以降のループでコールバッ
    クを呼び出します。 これは setTimeout(fn, 0) の
    単純なエイリアスではなく、 はるかに効率的で
    す。
for (var i = 0; i < 1024*1024; i++) {       処理時間
  process.nextTick(function (){
   Math.sqrt(i); } );                   0.360u 0.072s 0:00.44 97.7%
}
                                            約5倍の差
for (var i = 0; i < 1024 * 1024; i++) {
  setTimeout(function () {
    Math.sqrt(i) }, 0);                 1.700u 0.800s 0:02.51 99.6%
}
       おそらくリンクリストの生成と時刻取得のオーバヘッドによるものだろう(未
node-v0.9に向けて isaacs からの提案
• process.nextTick()でイベントハンドラを追加するのはよくやること
  だけど
  次のイベントループでハンドラが登録されるまでの間にイベントが
  発生したりするとI/Oの取りこぼしが起きてしまう。
• 次のイベントが発生する前に確実にハンドラを登録をするために、
  V8でJSを実行した直後に process.nextTick() に登録された関数を全部
  実行するようにしたい。
• 再帰処理とかの展開もそこで行うので次のようなコードでは
  setTimeout() は起動しなくなるよ。
          setTimeout(function() {
           console.log('timeout');
          }, 1000);
          process.nextTick(function f() {
           process.nextTick(f);
          });
node-devでの大論争
      推進派                  擁護派
• 今までの動作がそもそもおか      • 別のAPIにすればいいじゃない
  しかった。正しい動作に変え        か
  るだけ                • 実際にコード変更するのがど
• CPU処理の分散のために再帰を      んなに大変か
  使うのは悪いこと、child     • どうせ今さら何言っても聞き
  process を使え          入れてくれないだろう
• idle用リスナの用途に再帰を使
  うのはわからんでもないが、
  setTimeoutを使え
• API名を変えるのはもう遅い
• 実際にI/Oの取りこぼしでバグ
  が出ている。この変更でそれ
  を直すのが優先する
今後どうなるのか(想像)
                                   終わり     始まり
                                                          setTimeout()
                                            1:時刻更新
                      7:ハンドル終了
                                                                nextTick()
nextTick()                                       2:run_timers    全展開
 全展開
               6:run_check

  コールバッ
                                   イベントループ
    ク                               一周(Tick)
                   5:poll                          3:run_idle

                                         4:run_prepare
             libev+kernel
             epoll: Linux
             kqueue: BSD
             event port: Solaris
                                         再帰は一定回数繰り返したら遅延させるかも
process.nextTickの正しい使い方
var events = require('events');
var util = require('util');                 非同期イ
function Hoge() {                           ベントの
  var self = this;                           生成
  process.nextTick(function() {
    self.emit('foo');
  });
}
util.inherits(Hoge, events.EventEmitter);
var hoge = new Hoge();
hoge.on('foo', function() {
  console.log('foo event emitted');
});
process.nextTickの正しい使い方
var events = require('events');
var util = require('util');
function Hoge(cb) {                         非同期コール
  if(cb) {                                  バックの呼び出
    process.nextTick(function() {
     cb();                                     し
    });
  }
}
util.inherits(Hoge, events.EventEmitter);
Hoge.prototype.setfoo = function(arg) {
  this.foo = arg;
};
var hoge = new Hoge(function() {
  hoge.setfoo('bar');
  console.log(hoge.foo);
});
process.nextTickの再帰を避ける
var cluster = require('cluster');
if (cluster.isMaster) {                  CPU消費処理は
  var worker = cluster.fork();            子プロセスで
  worker.on('message', function(msg) {
    console.log(msg);
  });
} else {
  //子プロセス
  while(1) {
    process.send(‘hoge’);
  }
}
まとめ
• Node のイベントループの仕組みを良く理解
  した上でイベントループを止めないことを意
  識してコードを書きましょう。
• process.nextTick() は、
 – 非同期イベントの発生
 – 非同期コールバックの実行
 の用途で使いましょう。
• CPUを消費する処理には、child process を利
  用しましょう。
• node-v0.9 では process.nextTick()の動作仕様
  が変わる予定です。

More Related Content

そうだったのか! よくわかる process.nextTick() node.jsのイベントループを理解する

  • 1. そうだったのか! よくわかる process.nextTick() Node.jsのイベントループを理解する IIJ 大津 繁樹 (@jovi0608) 2012年6月28日 東京Node学園6時限目
  • 3. Nodeの歩み(参考) 2007/10 libev公開 2010/08 nodejs_jp開始 2008/05 libeio公開 2010/09 no.de開始 2009/09 Google V8公開 2010/11 Joyent管轄へ 2009/02 ry Node開発開始 2011/02 node-v0.4.0リリース 2009/05 node-v0.0.1リリース 2011/03 東京Node学園#1 2009/06 nodejs ML開始 2011/10 東京Node学園祭 2009/10 npm公開 2011/11 node-v0.6.0リリース 2009/11 JSConf EU ry発表 2011/12 Azureサポート 2010/04 Herokuサポート 2012/01 isaacs管理へ 2010/08 node-v0.2.0リリース 2012/06 node-v0.8.0リリース
  • 4. 今日の話 • Nodeのイベントループとは • process.nextTickとは • node-devでの大論争 • 今後どうなる? • process.nextTickの正しい使 い方 おそらく世界初?の Node-v0.8ベースでイベント ループを解説 (libuvの大幅な変更に追随) (注: 説明はLinuxが対象です。)
  • 5. Nodeのイベントループとは、 • Node の心臓 イベントループが終了したら Node は死にます。
  • 6. Nodeのイベントループの正体 Node が起動する時に uv_run() が呼 ばれます。(src/node.cc:2910) https://github.com/joyent/node/blob/v0.8.0-release/deps/uv/src/unix/core.c#L265
  • 7. イベントループが回り続けるには アクティブな handle/req がなけれ ば イベントループが終了 https://github.com/joyent/node/blob/v0.8.0-release/deps/uv/src/unix/core.c#L252-261
  • 8. handle と req の違い • handle – I/O が発生してない時でもイベントループを 維持 – (例) server.listen() • req – I/Oが発生している時だけイベントループを 維持 – (例) http.get()
  • 9. handle と req の種類 handle req ASYNC 非同期ジョブの操作 CONNECT stream接続 CHECK ループの最後の操作 WRITE stream書き込み FS_EVENT ファイルイベント操作 SHUTDOWN stream停止 FS_POLL statの問い合わせ操作 UDP_SEND udp 送信 IDLE アイドルの時の操作 FS ファイル操作 NAMED_PIPE 名前付きパイプの操作 WORK ワーカスレッド POLL fdイベントの操作 GETADDRINFO アドレス情報取得 PREPARE ループの最初の操作 PROCESS プロセスの操作 TCP TCPの操作 後で見て TIMER タイマー操作 おいて下 TTY UDP TTYPの操作 UDPの操作 さい。
  • 10. 実際のコードでは、(その1) var http = require('http'); アクティブ ハンドル var server = http.createServer(); 0 アクティブハンドルが無いからNode終了
  • 11. 実際のコードでは、(その2) var http = require('http'); var server = http.createServer(); アクティブ server.listen(1234); ハンドル追加 (+1) アクティブハンドルが作成されNode は終了しない。実際は epoll wait (Linux)して
  • 12. 実際のコードでは、(その3) var http = require('http'); var server = http.createServer(); server.listen(1234, function() { アクティブ server.close(); ハンドル削除 (+1-1=0) }); アクティブハンドルがすぐ無効化される のでNode終了
  • 13. イベントループの中身 7つのステップ 1. 時刻更新 2. タイマー実行 3. アイドル実行 4. Prepare実行 5. I/Oイベント実行 (libev) 6. Check実行 7. ハンドル終了
  • 14. Node-v0.8イベントループ概要 終わり 始まり setTimeout() nextTick() 1:時刻更新 7:ハンドル終了 6:run_check 2:run_timers コールバッ イベントループ nextTick() ク 一周(Tick) ユーザ 5:poll 3:run_idle プログラム 4:run_prepare libev+kernel epoll: Linux nextTick() kqueue: BSD event port: Solaris (注: ユーザプログラムは 3: run_idle から始まる
  • 15. イベントループを止めてはいけない! 終わり 始まり setTimeout() nextTick() 1:時刻更新 7:ハンドル終了 6:run_check 2:run_timers こんなコードはダメ! while(1) コールバッ ク while(1) { console.log(‘hoge’); 5:poll } 3:run_idle ずっとここ libev+kernel で止まる! 4:run_prepare epoll: Linux kqueue: BSD nextTick() event port: Solaris (注: ユーザプログラムは 3: run_idle から始まる。
  • 17. 理由1:呼び出し順番 setTimeout(function(){ console.log(‘3:foo’); $ node tick-order.js }, 0); 1:piyo process.nextTick(function() { 2:hoge console.log(‘2:hoge’); 3:foo }); console.log(‘1:piyo’); setTimeout() より process.nextTick() が先に呼ばれる (注: 将来仕様が変わる可能性があります。)
  • 18. 理由1:呼び出し順番 終わり 始まり setTimeout() console.log(‘3:foo’) nextTick() 1:時刻更新 7:ハンドル終了 6:run_check 2:run_timers console.log(‘1:piyo’) コールバッ イベントループ nextTick() ク 一周(Tick) 5:poll 3:run_idle 4:run_prepare console.log(‘2:hoge’) nextTick() (注: ユーザプログラムは 3: run_idle から始まる。)
  • 19. 理由2:入れ子の呼び出し順番 process.nextTick(function() { setTimeout(function(){ console.log(‘4:foo'); $ node tick-order2.js }, 0); 1:piyo process.nextTick(function() { 2:bar console.log(‘3:hoge'); 3:hoge }); 4:foo console.log(‘2:bar'); }); console.log(‘1:piyo’); process.nextTick() のスコープ内でも setTimeout() より process.nextTick() が先に呼ばれる (注: 将来仕様が変わる可能性があります。)
  • 20. 理由2:入れ子の呼び出し順番 終わり 始まり console.log(‘3:hoge’) setTimeout() console.log(‘4:foo’) nextTick() 1:時刻更新 7:ハンドル終了 6:run_check 2:run_timers console.log(‘1:piyo’) コールバッ イベントループ nextTick() ク 一周(Tick) 5:poll 3:run_idle 4:run_prepare console.log(‘2:bar’) nextTick() (注: ユーザプログラムは3: run_idleから始まる。)
  • 21. process.nextTick()の説明(マニュアルより) イベントループの次以降のループでコールバッ クを呼び出します。 これは setTimeout(fn, 0) の 単純なエイリアスではなく、 はるかに効率的で す。 for (var i = 0; i < 1024*1024; i++) { 処理時間 process.nextTick(function (){ Math.sqrt(i); } ); 0.360u 0.072s 0:00.44 97.7% } 約5倍の差 for (var i = 0; i < 1024 * 1024; i++) { setTimeout(function () { Math.sqrt(i) }, 0); 1.700u 0.800s 0:02.51 99.6% } おそらくリンクリストの生成と時刻取得のオーバヘッドによるものだろう(未
  • 22. node-v0.9に向けて isaacs からの提案 • process.nextTick()でイベントハンドラを追加するのはよくやること だけど 次のイベントループでハンドラが登録されるまでの間にイベントが 発生したりするとI/Oの取りこぼしが起きてしまう。 • 次のイベントが発生する前に確実にハンドラを登録をするために、 V8でJSを実行した直後に process.nextTick() に登録された関数を全部 実行するようにしたい。 • 再帰処理とかの展開もそこで行うので次のようなコードでは setTimeout() は起動しなくなるよ。 setTimeout(function() { console.log('timeout'); }, 1000); process.nextTick(function f() { process.nextTick(f); });
  • 23. node-devでの大論争 推進派 擁護派 • 今までの動作がそもそもおか • 別のAPIにすればいいじゃない しかった。正しい動作に変え か るだけ • 実際にコード変更するのがど • CPU処理の分散のために再帰を んなに大変か 使うのは悪いこと、child • どうせ今さら何言っても聞き process を使え 入れてくれないだろう • idle用リスナの用途に再帰を使 うのはわからんでもないが、 setTimeoutを使え • API名を変えるのはもう遅い • 実際にI/Oの取りこぼしでバグ が出ている。この変更でそれ を直すのが優先する
  • 24. 今後どうなるのか(想像) 終わり 始まり setTimeout() 1:時刻更新 7:ハンドル終了 nextTick() nextTick() 2:run_timers 全展開 全展開 6:run_check コールバッ イベントループ ク 一周(Tick) 5:poll 3:run_idle 4:run_prepare libev+kernel epoll: Linux kqueue: BSD event port: Solaris 再帰は一定回数繰り返したら遅延させるかも
  • 25. process.nextTickの正しい使い方 var events = require('events'); var util = require('util'); 非同期イ function Hoge() { ベントの var self = this; 生成 process.nextTick(function() { self.emit('foo'); }); } util.inherits(Hoge, events.EventEmitter); var hoge = new Hoge(); hoge.on('foo', function() { console.log('foo event emitted'); });
  • 26. process.nextTickの正しい使い方 var events = require('events'); var util = require('util'); function Hoge(cb) { 非同期コール if(cb) { バックの呼び出 process.nextTick(function() { cb(); し }); } } util.inherits(Hoge, events.EventEmitter); Hoge.prototype.setfoo = function(arg) { this.foo = arg; }; var hoge = new Hoge(function() { hoge.setfoo('bar'); console.log(hoge.foo); });
  • 27. process.nextTickの再帰を避ける var cluster = require('cluster'); if (cluster.isMaster) { CPU消費処理は var worker = cluster.fork(); 子プロセスで worker.on('message', function(msg) { console.log(msg); }); } else { //子プロセス while(1) { process.send(‘hoge’); } }
  • 28. まとめ • Node のイベントループの仕組みを良く理解 した上でイベントループを止めないことを意 識してコードを書きましょう。 • process.nextTick() は、 – 非同期イベントの発生 – 非同期コールバックの実行 の用途で使いましょう。 • CPUを消費する処理には、child process を利 用しましょう。 • node-v0.9 では process.nextTick()の動作仕様 が変わる予定です。