そうだったのか! よくわかる 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が対象です。)
- 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の操作
さい。
- 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 から始まる。
- 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()の動作仕様
が変わる予定です。