Closed19

読む:JS基礎いろいろーイベントループ

Yug (やぐ)Yug (やぐ)

え!JSって同期が当たり前で非同期やりたいならasync/awaitとか使う、ていう感じだと思ってた!非同期が基本なのか!?

JSは基本的にシングルスレッドで非同期実行、というメカニズムになっています。

いや、ちょっと待て、恥ずかしすぎるが、そもそも同期と非同期ってなんだっけ?復習してくる
(Promiseとかasync/awaitもよくわかってないのでそこらへんも復習してくる)
https://zenn.dev/yg_kita/scraps/97bc570eb5a69c

Yug (やぐ)Yug (やぐ)

戻ってきた

やはり「JSは基本的に非同期実行」という記述はおかしいはず。(シングルスレッドという記述は合ってる)

「基本は同期処理だが、async/awaitやthen/catchなどを使うことでfetchなどの非同期処理にも対応できるようにはなっている」

という表現が正しいはず。

その非同期処理を検知したらマクロタスク(タスクキュー)/マイクロタスク(ジョブキュー)に入れられるが、非同期処理でなければただ同期的にコールスタックから処理されるだけなので

Yug (やぐ)Yug (やぐ)

わかりやすい

このように、店員さんが一人(シングルスレッド)で順番に注文を聞いていく(リクエストを一つずつ送る)、最終的に料理が出される(レスポンスが帰ってくる)が、順番は注文内容や大将の都合次第なので必ず注文順とは限りません(レスポンスがランダム順に返ってくる)、というのがjsの実行環境とよく似ています。

Yug (やぐ)Yug (やぐ)

へぇ

ブラウザー環境では、複数のスレッドによる協同作業が行われています。例えば:

  • GUIレンダリングスレッド
  • JSエンジンスレッド
  • イベントリスニングスレッド
  • タイマースレッド
  • HTTPリクエストスレッド
  • 他いろいろ

そんなにブラウザってスレッドあるのか...

ブラウザーで1つのタブだけを開いても、スレッド数が数十ないし百まで行きます。

なるほど

ここのJSがシングルスレッドだ、というのは、上記の複数のスレッドを利用し、切り替えはしているが、同時に実行するスレッドが一つだけとのことです。

Yug (やぐ)Yug (やぐ)

ほぉ

ここでシンプルに上記のスレッドを分けると:

  • メインスレッド:画面のレンダリング、jsコードの実行、イベント発火など
  • ワーカースレッド:よくバックグランドとか言われますが、主に非同期のタスクの処理
Yug (やぐ)Yug (やぐ)

4つだけだと思ってたが、Worker Threadってやつがあるな、タスクキューとの違いが気になる(タスクキューも非同期処理が積まれるはず)

Yug (やぐ)Yug (やぐ)

なるほどなぁ

処理順番で言えば:

  • メインスレッドでコードを解釈(interpret)し始める
  • 同期コードは直接コールスタックで実行
  • 非同期コードはワーカースレッドに預ける
  • 実行待ちの非同期タスクをタスクキューに投げる
  • コールスタックの関数がなくなるまで実行
  • コールスタックが空っぽになると、タスクキューから非同期のタスクを入れる
  • 非同期タスクを順次に実行

メインスレッドから直接タスクキューに非同期処理を渡さずに、わざわざワーカースレッドというやつをかます必要性がわからんな

あとコールスタックがスタックであるイメージがわかない。キューみたいに順番に実行されてるようにしか思えないんだが。早く呼ばれた方が当然早く実行されてるので、スタックではなくキューな感じがしてしまう。スタックとしてのFILOな例を知りたい

Yug (やぐ)Yug (やぐ)

あーそういうことか、ワーカースレッドの必要性がわかった

2と3はタイマーがありますが、3の方が短かったので、先に時間となり、ワーカースレッドより、タスクキューに投げられます。

タイマーを見て、切れたらタスクキューに渡してあとは実行するだけっていう役割を担ってるのか。

そのままタスクキューに入れちゃうと、タスクキューはただ順番に実行するだけなのでどっちが早くタイマー切れるかとか関係なしに、重い非同期処理から実行始まったりしてしまう可能性があるのか

Yug (やぐ)Yug (やぐ)

あ、コールスタックの方のスタックとしての疑問も解消できた。なるほど

上記の例で一つ注意すべきなのは、タスク1と4は全部コールスタックに入ってから実行されていくわけではなく、入った途端に実行し、その後すぐにスタックから離れてなくなります(GC)。
ただ、コールスタックにいっぱい関数が貯まるケースもあります。それは、関数の中で関数を呼び出すケースです。

要するに、関数の中で関数を呼び出すと、親関数の実行が終わっていないから、コールスタックの中に残されます。子関数の実行に入るときに、親関数の上に来ますので、最後に入った子関数が実行されてスタックから離れていきます。

関数の中で関数を呼ぶ場合はスタックの動作になるな、たしかに

Yug (やぐ)Yug (やぐ)

うわ、ほんとだ1万くらいでエラーになった

再帰のパターンでは検証出来ますが、コールスタックには、最大「深さ」があります。
この深さはブラウザーによって違いますが、1万ほどが最大のようです。

Yug (やぐ)Yug (やぐ)

うわーほんとだできた。それは賢いなぁ。

このスタックの制限を超えるために、コードを多少アレンジすることができます。
setTimeout関数で、taskの再帰実行をタスクキューに投げるように変更します。すると、task関数がコールスタックに溜まっていくのではなく、タスクキューに並べて順番にスタックに入ります。

ただめっちゃ実行速度遅かった。
わざわざワーカースレッド->タスクキューという経由を発生させてるので、それが時間かかってるのだろう

Yug (やぐ)Yug (やぐ)

あーてかワーカースレッドって、Web API(setTimeoutとか)そのものの実行を指すのかな。

で、その実行によってタイマーが終了したことを検知したらそのWeb APIのコールバック関数をタスクキューに投げる、みたいな

Yug (やぐ)Yug (やぐ)

うん、イベントループが理解できた気がする

ここまでのおさらいとして、イベントループのモデルとは、コード解釈→同期はコールスタックで実行、非同期はワーカースレッド→ワーカースレッドが非同期タスクをキューに投げる→コールスタックが空の状態で、タスクキューから新しいタスクがスタックに入る、となります。このプロセスは、先ほどの再帰+setTimeoutで検証できたように、不可抗力がない限り永遠に続けて行けます。

「メインスレッドで処理がシングルスレッドで実行されていくにあたって、同期処理はコールスタックで実行、非同期処理はワーカースレッド->タスクキューで実行されていく、これをループし続ける」

という流れのことをイベントループと呼ぶ感じだな

Yug (やぐ)Yug (やぐ)

へぇ

まぁとにかくマクロタスクよりマイクロタスクが優先であるということ

んでもっと細かい話がこれ

マクロタスクの中でマイクロタスクがある場合(コード例に参照)、該当マイクロタスクは、次のマクロタスクの順番の前まで優先に実行されます。もしマクロタスクのなかにさらにマクロタスクがある場合、それが次のマクロタスクとなります。

Yug (やぐ)Yug (やぐ)

てかこの記事と言ってること違う?
https://qiita.com/ryosuketter/items/dd467f827c1b93a74d76#macrotasksマクロタスクスとmicrotasksマイクロタスクス

今回の記事だとマクロタスクもマイクロタスクもタスクキューに属するということになるはずだが、前の記事だとマクロタスクだけがタスクキューであって、マイクロタスクはジョブキューであると分けているんだよなぁ

まぁタスクキュー/ジョブキューというのはただの名称であって、実体としては2つキューがあるということは同じ、みたいな話なのかも

タスクキューを2つあると描くのも良いのですが、こちらの図で、マイクロとマクロが具体的にどのように実行順番が決められているのかがよりはっきりと表現できると思います。2つのキューで描く場合、毎回マクロキューのタスクを実行する前に、マイクロキューにタスクがあるかどうかのチェックが必要なので、その辺りの表現も不可欠でしょう。

Yug (やぐ)Yug (やぐ)

ふむ

マイクロタスクはECMAスタンダードの更新で追加された新しい非同期タスクのことで、毎回マクロタスクの実行する前に、マイクロタスクがあるかどうかはチェックされます。

例外もあるっぽい

つまり、マイクロタスクの実行は、基本的にマクロタスクより優先されます。ただ、この優先順位は、マクロ・マイクロだけで決められるわけではなく、タスクがワーカースレッドによって処理されたタイミングにも影響されますので、次の節で例で説明します。

Yug (やぐ)Yug (やぐ)

なるほどなぁ

ここで注意したいのは、イベントリスナーでレジストされたコールバックも、マクロタスクとなります。つまり、上記の例では、マクロタスクの中(コールバック)にマイクロタスクがある、というパターンとなります。開発中にもよく見られますが、イベントをレジストして、クリックイベントとかがあるときに、データをfetchAPIとかで取得する、という場面です。
先ほどの図で当てはめていくと、マイクロタスク→マクロタスク→マイクロタスク→マクロタスク→マイクロタスクがわかるので、出力が、5-2-1-4-3となります。

全部マイクロタスクを見たうえで、それらすべて優先する、という訳ではないのがおもしろい

あくまでマイクロ->マクロ->マイクロ->マクロという順番になってるのか

Yug (やぐ)Yug (やぐ)

これおもろい
https://qiita.com/nuko-suke/items/5b16ab9de402547c5797

なるほどたしかにぃ、それ面白いな

正確には、 setTimeout は指定した時間に処理が走らせるのではなく、 指定した時間にタスクキューにいれる のです。
setTimeout(main, 3000) の例では、 3 秒後に main 関数がタスクキュー( TODO リスト)にいれられます 。
このため、他のタスクで忙しい時は、 3 秒後に実行されるとは限らないのです 。

コールスタックが終わった後タスクキューにある非同期処理のコールバック関数はコールスタックに積まれて順次実行されていくわけだが、それが大量にあったらコールスタック内で少し遅延してしまうみたいなこともあり得るのか、なるほど

このスクラップは10日前にクローズされました