🚖
イベントループについて

mokun632

mokun632

2022年4月5日
js

# モチベーション

昔、JavaScript の非同期処理の裏側ではどういった処理が行われているのか疑問に思いました。

当時、新しくスレッドを作って実行しているのかと思ったのですが、そもそも JavaScript はシングルスレッドなので、新しくスレッドを作る事は出来ないので、どうやって非同期処理を実行しているんだろうと思い、 調べていくうちにイベントループが非同期な処理を実現していることを知り、その時はざっくり理解した気になって満足していました。

ですが、最近になってイベントループの TaskQueue は 2 種類存在しているという事を知り、イベントループへの理解が甘いと思い、また改めて学習し直そう思ったのが、モチベーションになります。

# ざっくりイベントループについて

シングルスレッドで並行処理を実現できます。

イベントループは JavaScript ランタイムがプログラムを実行するときの基盤となる仕組みです。

# JavaScrip はシングルスレッド

イベントループの説明に入る前に前準備として、

まず、JavaScript の大きな特徴として「シングルスレッド」と言う点があります。つまり一度に実行できるタスクは1つになります。例えば、30 秒以上かかるタスクを実行した場合、そのタスクを実行している間は、他のタスクは実行を待つことになります。

JavaScript はデフォルトでブラウザのメインスレッドと呼ばれる描画を処理するスレッドが使用されます。その為、時間のかかるタスクがある場合、UI 全体が動かなくなってしまい、大きく UX を損なってしまいます。

# イベントループの構成

イベントループは主に 3 つの要素を備えています。

CallStack、TaskQueue、Web API になります。

下記で、それぞれ簡単に説明していきます。

# CallStack

関数は呼び出されると CallStack に追加されます。
CallStack は LIFO(後入れ先出し)で機能します。
JavaScript エンジンの内部に実装されています。

# TaskQueue

コールバック関数は TaskQueue で待機します。
TaskQueue は FIFO(先入れ先出し)の配列で、イベントループは、CallStack が空になる度に、TaskQueue からコールバック関数を取り出して実行します。
JavaScript エンジンの外部に実装されています。
こちらは、TaskQueue と MicroTaskQueue の 2 種類あります。

# Web API

Web API には DOM Event, setTimeout, Ajax などが含まれます。これらの機能は非同期(ノンブロッキング)に実行されます。
ブラウザが提供する機能です。

上記の3つの要素が下記図の様に構成されています。

イベントループ
引用: JavaScript の非同期処理 Promise、Async と Await の仕組みを GIF アニメで解説

# イベントループの動き

先ほどの図を踏まえて、コードを動かしてみて、実際のイベントループの動きを解説していきたいと思います。
わかりやすいように、コンビニにて、客が弁当を購入する時の店員の対応を下記コードに表しました。

const task1 = () =>
  console.log("弁当のバーコードを読み取り、電子レンジに入れる");
const task2 = () => setTimeout(() => console.log("客に弁当を渡す"), 1000);
const task3 = () => console.log("お会計をする");

task1();
task2();
task3();

上記コードで行われていることは、下記のようになります。

  1. task1 が CallStack に積まれます。
  2. task1 実行され CallStack からポップされます。
  3. task2 が CallStack に積まれます。
  4. task2 が実行され、setTimeout が CallStack に積まれます。
  5. setTimeout が実行され、コールバック関数が Web API に移動し、CallStack から setTimeout がポップされます。
  6. task2 がポップされます。
  7. Web API で指定されたタイマーが実行され、その間、task3 が CallStack に積まれます。
  8. task3 が実行され、CallStack からポップされます。
  9. Web API からタイマー終了後、setTimeout のコールバック関数が TaskQueue にエンキューされます。
  10. CallStack が空である事を確認したら、setTimeout のコールバック関数が TaskQueue からデキューされ、CallStack に積まれます。
  11. setTimeout のコールバック関数が実行され、CallStack からポップされます。

以上がイベントループの動きです。

ただ、上記コードは、実際の実務などでのコードとはだいぶ乖離していてイメージしにくいと思いますので、実際に存在しない架空のコードを下記に書いてみました。

const amount = BarcodeReader.read(bento); // 弁当のバーコードを読み取る
microwaveOven.chin(bento, (res) => getHotBento(res)); // 弁当を温める
cashier.payment(amount); // お会計をする

microwaveOven.chin()が非同期になっています。

引数で渡されるコールバック関数の getHotBento()は、弁当を温めた後、TaskQueue にエンキューされます。

弁当を温めている間に cashier.payment()が実行されお会計します。

そして、最後に getHotBento()がデキューされ、CallStack で実行されます。

# 長所

  • 時間のかかる IO 処理などがあっても、処理を止めずにノンブロッキングで実行できる。
  • マルチスレッドの問題点(C10K 問題や、スレッドセーフへの配慮)を解決します。(Node.js の場合)(例として、Web サーバの nginx がイベントループで動作していて、16GB のメモリ で 1,000,000(10K の 100 倍)の同時接続に耐えられるそうです。)

# 短所

  • 制御フローが複雑になりやすい。

# おまけ

# Node.jd のイベントループについて

Web API はブラウザが提供する機能なので、Node.js に Web API は存在しません。
それでは、どうやってイベントループを実現しているかと言うと、システムカーネルに操作をオフロードすることで実現しています。

そして、Node.js は V8 を使用しているのですが、イベントループを提供しているのは、libuv と言う C ライブラリになります。

ですので、JavaScript のイベントループとは異なり、Node.js の Queue には 6 つのフェーズがあります。

興味のある方は、調べてみてください。

# 参考文献

  1. JavaScript の非同期処理 Promise、Async と Await の仕組みを GIF アニメで解説
  2. JavaScript イベントループの仕組みを GIF アニメで分かりやすく解説
  3. 並行モデルとイベントループ
  4. Web API の紹介
  5. The Node.js Event Loop, Timers, and process.nextTick()
  6. 今村 謙士 著『ハンズオン Node.js』オライリー・ジャパン