JavaScript (Node.js) の非同期処理とシングルスレッド


## 本記事の目的

[JavaScript] や [Node.js] はよくシングルスレッドだ〜、と言われますが、では非同期処理はどうやって実行されているのか ([Non-Blocking I/O]) をざっくりと (私の身内に) 説明する為のサンプルコードです。

> [Node.js], [V8]([libuv]) のコードレベルでちゃんと理解したいのであれば、以下のサイトが大変参考になりました。
> 
> * https://blog.hiroppy.me/entry/nodejs-event-loop

## 検証環境

* iMac (Retina 5K, 27-inch Late 2014), 4 GHz Intel Core i7
* [Node.js] v12.13.0

```shell
$ nodebrew install-binary v12.13.0
$ nodebrew use v12.13.0
```

> [ブラウザ JavaScript の Event loop](https://html.spec.whatwg.org/multipage/webappapis.html#event-loops) はまたちょっと違います。

## 早速サンプルコードから

以下の様な JavaScript ``index.js`` を、[Node.js] で実行します。

1. 【処理 1】ミリ秒で終わる処理を ``setTimeout()`` で 5 秒後に発火.
2. 【処理 2】ミリ秒で終わる処理を ``setTimeout()`` で 0 秒後に発火.
3. 【処理 3】10 秒かかる同期処理を実行.

> * 時間の計測には [Node.js] 標準 API の [perf_hooks] モジュールを使用しています。 [Node.js] プロセス実行開始からのミリ秒を得られます
> * コード中では、ミリ秒 → 秒、に変換して表示しています

```javascript:index.js
const { performance } = require('perf_hooks');

/**
 * @return 本スクリプトを実行してからの経過秒数.
 */
const seconds = () => performance.now() / 1000;
const secondsPadded = () => seconds().toFixed(6).padStart(10, ' ');  // 長さ揃える.

//////////////// 処理３つ ////////////////

/**
 * 処理 1 (非同期, 5 秒後に発火).
 */
const func1 = () => {
  console.log(`${secondsPadded()} seconds --> 処理 1 (非同期, 5 秒後に発火)`);
};

/**
 * 処理 2 (非同期, 0 秒後に発火).
 */
const func2 = () => {
  console.log(`${secondsPadded()} seconds --> 処理 2 (非同期, 0 秒後に発火)`);
};

/**
 * 処理 3 (同期. 10 秒かかる).
 */
const func3 = () => {
  while (seconds() < 10) { /* consuming a single cpu for 10 seconds... */ }

  console.log(`${secondsPadded()} seconds --> 処理 3 (同期, 10 秒かかる)`);
};

//////////////// 計測開始 ////////////////

console.log(`${secondsPadded()} seconds --> index.js START`);

// [非同期] 5 秒後に実行.
setTimeout(func1, 5000);

// [非同期] 即時実行.
setTimeout(func2);

// 同期実行.
func3();

console.log(`${secondsPadded()} seconds --> index.js END`);

//////////////// 計測終了 ////////////////
```

## 期待値？

なんとなく *「こう動作するだろう...」* という気分になるのは ↓ でしょう。

```
$ node index.js

  0.000000 seconds --> index.js START
  0.000000 seconds --> 処理 2 (非同期, 0 秒後に発火)
  5.000000 seconds --> 処理 1 (非同期, 5 秒後に発火)
 10.000000 seconds --> 処理 3 (同期, 10 秒かかる)
 10.000000 seconds --> index.js END

```

## 実際は...

現実はこうです。何故でしょうか。

```
$ node index.js 

  0.175104 seconds --> index.js START
 10.000085 seconds --> 処理 3 (同期, 10 秒かかる)
 10.000210 seconds --> index.js END
 10.000955 seconds --> 処理 2 (非同期, 0 秒後に発火)
 10.001161 seconds --> 処理 1 (非同期, 5 秒後に発火)
```

## シングルスレッドだから、順番に処理している

おおよそ、[Node.js] (+[V8]/[libuv]) の内部では ↓ のように処理がシングルスレッドで行われています。

1. JavaScript コンテキストの生成時にイベントループが生成されます
2. 最初のエントリ JavaScript ``index.js`` がタスクとして、未実行キューに乗ります
3. イベントループ
	1. 未実行キューから ``index.js`` タスクが取り出され、実行が開始されます
		1. ``setTimeout(処理1, 5秒)`` が実行され、【処理 1】がタイマーキューに追加されます
		2. ``setTimeout(処理2, 0秒)`` が実行され、【処理 2】がタイマーキューに追加されます
		3. 【処理 3】が同期的に実行され、10 秒間、CPU (シングルコア) を専有します
	2. ``index.js`` タスクの実行が終了します
4. イベントループ
	1. タイマーキューから *有効期限が切れたタスク【処理 2】* を取り出し、実行が開始されます
	2. 【処理 2】タスクの実行が終了します
5. イベントループ
	1. タイマーキューから *有効期限が切れたタスク【処理 1】* を取り出し、実行が開始されます
	2. 【処理 1】タスクの実行が終了します

> 実際はタイマー Phase はキューではない (FIFO でもない) ですが、説明の都合上そう表記しました。

![JavaScript Non-Blocking I_O Architecture.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/60131/f3658cba-98a3-299b-3fbc-384af4b13ce7.png "JavaScript Non-Blocking I_O Architecture.png")

> 要はイベントループにて、実行可能なタスクがあれば即時実行し、なければ I/O 待ち ([epoll]) をすることになります。

## 結論

つまり、``setTimeout()`` 等の非同期タイマー処理は...

* *指定した時間が来たら即座に Callback を実行する. (OS 割り込みみたいに)*

ではなく...

* 指定した時間を **過ぎてたら** Callback を **できるだけ早く** 実行する

ですね。

それは [Promise] や、Network Socket I/O 待ちである [fetch] でも同じで...

* **Callback が実行可能になってから (現在実行中の他の処理を待って) 順番が来たら (やっと) 実行開始する**

です。

## 参考文献

* [Node.jsでのイベントループの仕組みとタイマーについて](https://blog.hiroppy.me/entry/nodejs-event-loop)
* https://github.com/megmogmog1965/epoll-example/blob/master/main.c


[Node.js]:https://nodejs.org/
[JavaScript]:https://developer.mozilla.org/ja/docs/Web/JavaScript
[V8]:https://v8.dev/
[libuv]:http://docs.libuv.org/en/v1.x/
[Non-Blocking I/O]:https://nodejs.org/ja/docs/guides/blocking-vs-non-blocking/
[perf_hooks]:https://nodejs.org/docs/latest-v12.x/api/perf_hooks.html
[Promise]:https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Promise
[fetch]:https://developer.mozilla.org/ja/docs/Web/API/Fetch_API
[epoll]:https://linuxjm.osdn.jp/html/LDP_man-pages/man7/epoll.7.html