第三回 JavaScriptの非同期処理とシングルスレッド


まずはサンプルコードから

以下の様な JavaScript index.js を実行します。

  1. 【処理 1】ミリ秒で終わる処理を setTimeout() で 5 秒後に発火
  2. 【処理 2】ミリ秒で終わる処理を setTimeout() で 0 秒後に発火
  3. 【処理 3】10 秒かかる同期処理を実行
/** 本スクリプトの実行開始時間 */
const startTime = new Date().getTime()

/**
 * 本スクリプトを実行してからの経過秒数を取得する。
 *
 * @return {number} 経過秒数
 */
function getSeconds() {
  return (new Date().getTime() - startTime) / 1000
}

/**
 * 本スクリプトを実行してからの経過秒数を取得する(フォーマット済み版)。
 *
 * @return {number} 経過秒数
 */
function getSecondsFormatted() {
  return getSeconds().toFixed(6).padStart(10, ' ')
}

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

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

/**
 * 処理 3 (同期. 10 秒かかる).
 */
function func3() {
  while (getSeconds() < 10) {
    // consuming a single cpu for 10 seconds...
  }
  console.log(`${getSecondsFormatted()} seconds --> 処理 3 (同期, 10 秒かかる)`)
}


// メイン処理開始
console.log(`${getSecondsFormatted()} seconds --> index.js START`)

// 処理 1 (非同期, 5 秒後に発火)
setTimeout(func1, 5000)

// 処理 2 (非同期, 0 秒後に発火)
setTimeout(func2)

// 処理 3 (同期, 10 秒かかる)
func3()

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

期待値?

なんとなく 「こう動作するだろう...」 という思うのは下記でしょう。

  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

実際は...

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

  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 秒後に発火)

シングルスレッドなので順番に処理している

おおよそ、 JavaScript の内部では下記のように処理がシングルスレッドで行われています。

  • 最初のエントリ JavaScript index.js がタスクとして、未実行タスクに乗ります
  • 未実行タスクから index.js タスクが取り出され、実行が開始されます


  • setTimeout(処理1, 5秒) が実行され、【処理 1】がタイマータスクに追加されます
  • setTimeout(処理2, 0秒) が実行され、【処理 2】がタイマータスクに追加されます


  • 【処理 3】が同期的に実行され、10 秒間、CPU (シングルコア) を専有します
  • index.js タスクの実行が終了します
  • タイマータスクから 有効期限が切れたタスク【処理 2】 を取り出し、実行が開始されます


  • 【処理 2】タスクの実行が終了します
  • タイマータスクから 有効期限が切れたタスク【処理 1】 を取り出し、実行が開始されます
  • 【処理 1】タスクの実行が終了します


結論

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

指定した時間が来たら即座に Callback を実行する
ではなく

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

それは Promisefetch でも同じで

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