Goal
- Node.js Eventloopとは何かを知る
- JSとNode.jsの非同期APIを区別する
- NodejsではEventloopを止めてはいけないことを学ぶ
- 他の同期処理をベースとした言語と同じ書き方をしてはいけない
Q. 次の挙動は?
setTimeout(() => {
console.log('ok');
}, 0);
// 時間のかかる同期処理 => その極端な例として無限ループ
while (1) {}
A. 何秒待っても出力されない
同期処理の言語に慣れていると、すぐにokの出力を期待してしまうが...
Node.jsでのsetTimeout()
- Node.jsはJavaScriptエンジン(v8)を持っている
- 基本的にはJSのAPIを利用できる
- ただし一部はNode.jsのランタイムではNode core APIが実行される
-
setTimeout()
,setInterval()
,setImmediate()
はJSのTimerではなくNode core APIのTimerが実行される- これらはNode.js Eventloop内で処理される
JS/Node.jsの主な非同期処理
- JS
- Promise(ここでは
Object.observe()
として扱う1)
- Promise(ここでは
- Node.js
- I/O
- setImmediate
- setTimeout/setInterval
- process.nextTick
- EventEmitter ※
emit()
のタイミング次第で同期にもなる
Node.js Eventloop
- Node.jsの非同期処理は全てEventloop内で評価される
- Node.jsコミッターの大津さんが作成したv0.11の図が今でもわかりやすい
- Eventloopはユーザーランドの実行後に回るので、長い同期処理があるといつまでも非同期コールバックが評価されない
ということを内部実装を見て確認
Node.js: 始まりの場所
node ***.js
を実行すると...
- stdout,stderrのバッファリングを0に
node::Start(argc, argv)
- v8の初期化など
- isolate作成: chromeのタブと同じ
- context作成
-
LoadEnvironment()
でC++からユーザーランドのJSを実行(globalを生やすとかbootstrap処理の後)
- do-while
Node.js: libuv
- node.ccのdo-while内でuv_run()
- MacやLinux: deps/uv/src/unix/core.c
- Windows: deps/uv/src/win/core.c
- depsディレクトリはNode.jsを支えるサードパーティの置き場(v8など)
- uv_run()はlibuvというマルチプラットフォームなライブラリのAPI
- libuvのuv_run()のwhileループをEventloopと呼ぶ
Eventloop ≠ node.cc do-while?
- 初期のnode.ccはdo-whileループなどなくuv_run()を実行しているだけだった
- 今はnode.cc do-while内でlibuvのuv_run()を回す二重ループ構造
- これによりuv_run()終了後にemitされる
beforeExit
イベントのリスナーの中でEventloopを復活させることが可能
Eventloopの終了条件
- uv_run()のwhileループの終了条件を見れば良い
-
uv__loop_alive()
の戻り値が0になること - すなわち
uv__has_active_handles
とuv__has_active_reqs
が0 - activeなhandleまたはreqが無くなったとき
-
handle/req
特徴 | module例 | |
---|---|---|
handle | I/Oが発生していないときでもイベントループを維持 | Timers, server.listen() |
req | I/O発生中のみイベントループを維持 | fs, http |
Nodejsが終了する条件は?
コードの上から下まで実行された時-
登録した非同期処理が全て実行された時- 実行待ちの非同期処理をdeactivateすることもできるので偽
- Eventloopが終了、つまりactiveなhandle/reqが無くなった時
- 但し
beforeExit
イベントリスナー内でhandle/reqが追加されると復活 - もちろんuncaughtExceptionなどの例外終了やprocess.exit()などによる強制終了もある
- 但し
libuvとは
- クロスプラットフォームで非同期イベントの監視を担う
-
src/
以下でディレクトリ名でunix/winで分かれ、ゴリゴリのマクロで実装
-
- イベント例
- 期間
- timer
- idle状態
- 複数のfd(ファイルディスクリプタ)のstatus
- file(disk I/O)やsocket(network I/O)など
- SIGNAL
- 子プロセスの状態
- 別スレッドからの通知
イベントの監視順
- 記述したコードの上から下の順に非同期コールバックはキューに積まれる
-
uv_run()内の各箇所で評価
-
uv__run_timers()
でsetTimeout/setIntervalを評価 -
uv__io_poll()
でI/Oを評価 -
uv__run_check()
でsetImmediate()を評価
-
イベントの監視順(コードで確認)
order.js
setImmediate(()=>{
console.log('setImmediate');
});
var fs = require('fs');
fs.open('./README.md', 'r', (err, fd)=>{
console.log('fs.open');
fs.read(fd, Buffer.alloc(1000), 0, len = 1000, null, (err, bytesRead, buffer) => {
console.log('fs.read');
});
});
setTimeout(()=>{
console.log('setTimeout');
}, 1);
console.log('userland');
// 実行結果
// userland
// setTimeout
// fs.open
// setImmediate
// fs.read
process.nextTick/Promiseのタイミング
-
process.nextTick
はEventloop内ではなくユーザーランドの実行後 -
Promise
はJSレイヤーなので、Eventloop内ではなくユーザーランドの実行後 - どちらが先?
-
Promise
はObject.observe()
と同じ発火タイミングで、process.nextTick()
の方が先になる
-
process.nextTick/Promiseのタイミング(コードで確認)
nextTick_vs_promise.js
Promise.resolve().then(() => console.log('promise resolved'));
process.nextTick(() => console.log('next tick'));
// 実行結果
// next tick1
// promise1 resolved
(重要)非同期APIの順番は保証されない
- 各イベントの順番は保証されていない
- API互換の保証外の挙動のため、今後の修正で変わりうる
- 順番に依存しないような設計をすべき
Eventloopを止めるな!(NG)
stop_eventloop.js
const max = process.argv[2] - 0; // 十万以上でやばい
for(let i = 0; i < max; i++)
console.log('test');
-
process.memoryUsage()
などでメモリ利用量の変化を監視- rss(アプリケーションのメモリ使用量)がloop回数に比例して増大
- Eventloopが回らず、console.logがイベントキューに溜まり続けてメモリが圧迫されるため2
EventLoopを止めるな!(OK)
- while/forでユーザーランドで同期的に処理させるのではなく、非同期処理でEventloop内で処理をする
alive_eventloop.js
const max = process.argv[2] - 0;
let counter = 1;
const timeout = setInterval(() => {
if(counter++ > max) clearInterval(timeout);
console.log('test');
}, 1);
まとめ
- Eventloopとは、libuvによるイベント(主にfdやtimer)を監視し続ける仕組み
- 監視に順番はあるが保証外なので依存しないよう設計する
- NodejsではEventloopが回るよう意識して実装する必要がある
- 同期実行が長いとtimerが指定時に動かなかったりmemory leakぽい挙動をすることがある
-
但しfd=1(tty)への書き込みは同期的に行われる。console.logは内部的にprocess.stdoutで実装されているが、それが同期か非同期かは出力先によって異なる: https://nodejs.org/api/process.html#process_a_note_on_process_i_o ↩