LoginSignup
28
12

More than 1 year has passed since last update.

【JavaScript】非同期処理について1〜タスクキュー、コールスタック、イベントループ〜

Last updated at Posted at 2020-12-31

※当方駆け出しエンジニアのため、間違っていることも多々あると思いますので、ご了承ください。また、間違いに気付いた方はご一報いただけると幸いです。

同期処理と非同期処理の違い

一体何が同期で、何が非同期なのか。

それは、 「前の処理の終わりと、次の処理の開始」 です。

例えば


関数A

関数B
 
関数C

このような処理があった場合、同期処理では関数Aが処理開始し、関数Aの処理が終了したタイミングで、関数Bが開始されます。関数Bと関数Cも同様です。

非同期の場合は、関数Aの処理が開始し、関数のAの処理が終了する前に、関数Bの処理が開始されたりします。
この場合、関数Aは非同期で処理されています。

同期処理の例

function first(time) {
  const startTime = new Date();//処理開始時刻をセット
  let i = 0;
  while (new Date() - startTime < time) {//処理開始時刻と現在時刻の差がtimeになるまでループ。(つまり、処理開始からtimeミリ秒経過するまで)
    i++;
  }
  console.log(i);
}

first(3000);
console.log("second");
結果
//3秒経過後

12256187
second

最初に【関数first】が呼び出されます。
while処理の終了のタイミングで、console.log(i)が呼び出され、console.log(i)が終了した時点で、関数function first(time) 自体が終了するので、
次の関数console.log("second")が呼び出されています。

非同期処理の例

function first(time) {
  const startTime = new Date();
  let i = 0;
  while (new Date() - startTime < time) {
    i++;
  }
  console.log(i);
}

setTimeout(function () {//setTimeout(待機完了後実施される処理、待機時間)
  first(3000)
}, 2000)

console.log("second")

setTimeoutは引数に(待機完了後実施される処理、待機時間)を受け取ります。つまり、first(3000)は、2000ミリ秒待機してから実行されます。
同期処理と同じように、前の処理の終了を時に次の処理が実行されるなとするならば

//5秒経過後(待機分2000ミリ秒とループ分3000ミリ秒)

second
20100095

となりそうですが、結果は

second

//5秒経過後

20100095

となります。

【関数first】が終了する前に、先に【console.log("second")】が非同期で実行されています。
このように、非同期処理では、前の処理が終了する前に、次の処理が開始されます。

シングルスレッド

JavaScriptは基本的にメインのスレッドのみで処理がなされます。
これはどういうことかというと、基本的に処理の流れは一本道(シングルスレッド)で実施されているということです。
そのため、非同期処理を実現するには、非同期処理部分を一度メインスレッドから切り離します。

同期処理のイメージ

非同期処理のイメー

非同期処理を行うことによって、時間のかかる処理を後回しにして、先に次の処理をやってしまうのでプログラムの無駄な待機時間が少なくなり効率がよくなります。

コールスタックについて

メインスレッドはコールスタックによって管理されています。
【コールスタック】とは、ざっくりイメージでいうと【呼び出された関数が積まれていく箱】です。
これは、【後入れ先出し】で処理されます。後入れ先出しとは、【後に積まれた処理を、先に実行する】ということです。
関数が呼び出されたらコールスタックに積まれ、処理が終了した関数はコールスタックから追い出されます。
どういうことかと言いますと

main.html
<script>
  function a() {
    return b();
  }

  function b() {
    return c();
  }

  function c() {
    return "ok";
  }

  function output(fn) {
    console.log(fn);
  }

  output(a());

</script>

検証用に、この様なシンプルなhtmlを作成しました。

処理の実行順として、最初に【output関数】が呼び出されます。
呼び出されたらコールスタックに積まれます。

【output関数】は、引数で【関数a】を呼び出しているので、次に【関数a】がコールスタックに積まれます。
※output関数は、処理が完了していない(関数aからまだ戻り値を受け取っていない)ので、コールスタックから追い出されない。

【関数a】は内部で、【関数b】を呼び出しているので、次に【関数b】がコールスタックに積まれます。
※関数aは、処理が完了していない(関数bから戻り値を受け取っておらずreturnはまだしていない)ので、コールスタックから追い出されない。

同様に【関数c】も積まれます。

関数cは

return "ok";

を実行します。

そうすると、【関数c】は処理が完了したので、コールスタックから追い出されます。

【関数b】は、【関数c】から戻り値 "ok" を受け取るので、それをreturnします。関数bの処理が終了するので、関数bはコーススタックから追い出されます。

同様に、【関数a】もコーススタックから追い出されます。

そして最終的、【関数output】も処理が終了し、”ok"がログ出力されて、【関数output】もコールスタックから追い出されて、コーススタックは空となります。

このコールスタックが積まれている状態とは、つまりメインスレッドが占有されている状態となり、非同期処理は、割り込むことができません。
シングルスレッドなので、あくまで処理の実行はメインスレッドでのみ行われます。
例えば、下記の様なプログラムがあるとします。

  function first() {
    const startTime = new Date();
    let i = 0;
    while (new Date() - startTime < 5000) {
      i++;
    }
    console.log(i);
  }

  function second() {
    console.log("ok");
  }

  setTimeout(function () {//setTimeout(待機完了後実施される処理、待機時間)
    second();
  }, 3000)

  first();

【関数second】自体は、3000ミリ秒の時間だけ待機しますが、メインスレッドが5000ミリ秒間、【関数first】に占有されているので、割り込むことができず、secondが出力されるのはプログラム開始から5000ミリ秒後となります。

この流れを図で説明すると、

プログラム開始するとsetTimeoutがコールスタックに積まれます。

【setTimeout】は、【第一引数の処理を、指定の時間、一旦隔離する】のが仕事です。
よって、関数がsecondが待機時間3000ミリ秒という情報付きでメインスレッドから隔離されます。

setTimeoutは、【一旦隔離する】のが仕事ですので、関数secondを隔離した時点で、処理は終了なので、コールスタックから追い出されます。

次に【関数first】が呼び出されて、コールスタックに入れられます。
(関数firstは5000ミリ秒間メインスレッドを占有します。)

プログラム開始後、3000ミリ秒経過すると【関数secondの待機時間】が解かれます。待機時間が解かれた関すは一旦、【キュー】に入ります。

もしこの時、コールスタックが空であれば、コールスタックに入るのですが、現状【関数first】が占有しているので、キューで待機します。

5000秒経過すると、【関数first】の処理が完了し、(iをログ出力し)コールスタックから追い出されます。

コールスタックが空になったので、【関数second】がコールスタックに入れられます。

そして、【関数second】も処理を実行し、("ok"をログ出力)コールスタックから追い出されます。

なお、キューは先入れ先出しとなります。
【先に順番待ちした方】が、【先に処理される】ということです。

  function first() {
    const startTime = new Date();
    let i = 0;
    while (new Date() - startTime < 5000) {
      i++;
    }
    console.log(i);
  }

  function second() {
    console.log("2nd ok");
  }

  function third() {
    console.log("3rd ok");
  }

  setTimeout(function () {//setTimeout(待機完了後実施される処理、待機時間)
    second();
  }, 3000)

  setTimeout(function () {//setTimeout(待機完了後実施される処理、待機時間)
    third();
  }, 2000)


  first();

この様なプログラムを実行した場合、結果は

17066850
3rd ok
2nd ok

先にメインスレッドから隔離されるのは、【関数second】ですが、待機が解かれるのは、【関数third】の方が早いので、先にキューに入れられます。
よって、コールスタックが空になった場合、【関数third】が、先にキューからコールスタックに移動するので、【関数third】の方が先に実行されるわけです。

※thirdが実行完了しコールスタックから追い出されるまでの間は、**コールスタックは空ではない(thirdがメインスレッドを占有)**のでsecondがコールスタックに入れられることはない。
あくまでsecondeがコールスタックに入るのはthirdの処理が終了して、コールスタックから追い出されて、コールスタックが空になった時

イベントループについて

このコールスタックの空き状況の確認と、キューへの伝達を行っているのが **「イベントループ」**と呼ばるものになります。

イベントループは図の様に、コールスタックとキューを巡回して監視し、コールスタックが空であれば、その事をキューに伝えます。

なお、このコールスタックの挙動はクロームの開発ツールから確認することができます。

sourcesタブを開き、該当するファイルを選択して、プログラム開始時にブレークポイントを設置します。
image.png

最初の処理でブラウザが一時停止します。

image.png

callstackの欄に、現在のコールスタックの積み重ね状況が表示されます。
(グローバルコンテキストの場合は、anonymousと表示されます。)
image.png

image.png

「step」 ボタンを押下するとに、処理を進めることができます。

次に続きます。

また、下記の記事でより詳細に非同期処理を記載いたしました。
もしよければ、下記の記事もご参考ください。

28
12
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
28
12