Help us understand the problem. What is going on with this article?

内部実装から読み解くNode.js(v11.0.0) Eventloop

More than 1 year has passed since last update.

内部実装から読み解くNode.js(v11.0.0) Eventloop

by darai0512
1 / 23

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)
  • 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を実行すると...

  1. src/node_main.cc: main(argc, argv)
    • stdout,stderrのバッファリングを0に
    • node::Start(argc, argv)
  2. src/node.cc: Start(argc, argv)
    • v8の初期化など
  3. src/node.cc: Start(event_loop, args, exec_args)
    • isolate作成: chromeのタブと同じ
  4. src/node.cc: Start(isolate, data, args, exec_args)
    • context作成
    • LoadEnvironment()でC++からユーザーランドのJSを実行(globalを生やすとかbootstrap処理の後) - do-while

Node.js: libuv

  • node.ccのdo-while内でuv_run()
  • depsディレクトリはNode.jsを支えるサードパーティの置き場(v8など)
    • uv_run()はlibuvというマルチプラットフォームなライブラリのAPI
  • libuvのuv_run()のwhileループをEventloopと呼ぶ

Eventloop ≠ node.cc do-while?


Eventloopの終了条件

  • uv_run()のwhileループの終了条件を見れば良い
    • uv__loop_alive()の戻り値が0になること
    • すなわちuv__has_active_handlesuv__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内ではなくユーザーランドの実行後
  • どちらが先?
    • PromiseObject.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ぽい挙動をすることがある


  1. Object.observe()とNode.jsのイベントループの関係 

  2. 但しfd=1(tty)への書き込みは同期的に行われる。console.logは内部的にprocess.stdoutで実装されているが、それが同期か非同期かは出力先によって異なる: https://nodejs.org/api/process.html#process_a_note_on_process_i_o 

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした