JavaScript | 非同期処理:非同期の基礎概念 – JavaScript がシングルスレッドである理由

JavaScript JavaScript
スポンサーリンク

まず「シングルスレッド」のイメージから

JavaScript が「シングルスレッド」というのは、
「同時に実行できるのは、基本的に1つのことだけ」 という意味です。

  • console.log() を実行しているあいだは、他の関数は同時には動かない
  • 一個の処理が終わってから、次の処理が始まる

画面のクリック処理も、タイマーの処理も、あなたの書いた関数も、
ぜんぶ「1本の道」に並んで順番に流れていきます。

ここが重要です。
JavaScript は「一人で全部やる店員さん」スタイル
その代わり、後ろで「手伝ってくれる人(ブラウザやOSの機能)」をうまく使うことで、
「シングルスレッドだけどノンブロッキング」という動きを実現しています。


なぜブラウザの JavaScript はシングルスレッドで始まったのか(歴史と事情)

元々「小さなスクリプト」からスタートした

JavaScript が作られた当初(1995年ごろ)、
想定されていたのは、次のような用途でした。

  • ボタンを押したときにちょっとした処理をする
  • 簡単なフォームチェックをする

今みたいな「巨大なアプリ」ではなく、
「HTMLにちょこっと動きをつける程度」 の役割だったんです。

そのときに大事にされたのは、

  • ブラウザ側・実装者側から見てシンプルであること
  • Web デザイナーや初学者でも簡単に触れること

でした。

マルチスレッドはパワフルですが、

  • ロック(同時アクセスを防ぐ仕組み)
  • デッドロック(お互い待ち合って詰む)
  • 共有データの整合性

などの難しい問題がつきまといます。

この複雑さを、初めての Web スクリプト言語に持ち込みたくなかった
という背景があります。

Web ページの世界では「安全性」が最優先だった

ブラウザの中には、

  • DOM(画面の要素)
  • cookie
  • localStorage

など、ユーザーの情報や状態に関わるものがたくさんあります。

これらを複数スレッドで同時に触り始めると、

  • どのスレッドがいつ DOM をいじるのか
  • 更新の途中の不整合状態が見えてしまわないか
  • 競合(同時更新)をどう防ぐのか

といった問題が一気に難しくなります。

シングルスレッドなら、
「いつ DOM が書き換わるか」は“実行中のその1本のコードの中だけ”で完結します。
ブラウザ実装者にとっても、セキュリティや安定性を確保しやすい設計でしたr

ここが重要です。
ブラウザの JavaScript は、
「安全でシンプルな UI スクリプト」としてスタートしたからこそシングルスレッド
そしてその設計が、今もなお引き継がれている、という流れです。


「シングルスレッドなのに非同期」ができる理由

1本しかないのに、なぜ「同時に動いてるように見える」のか

ここが多くの人が一度つまずくポイントです。

ポイントは、
「JavaScriptエンジン自体は1本だけど、その外側に“手伝い要員”がいる」
という構造にあります。

ブラウザ環境だと、

  • JavaScriptエンジン(シングルスレッド)
  • Web API(タイマー、DOM、XHR/fetch などを担当するブラウザ側の機能)
  • タスクキュー(「終わったらこれ実行してね」と並べておく箱)
  • イベントループ(キューから順に仕事を持ってくる係)

という役者が協力して動きます。

簡単な具体例:setTimeout の裏側で起きていること

console.log("A: 開始");

setTimeout(() => {
  console.log("B: 3秒後の処理");
}, 3000);

console.log("C: 同期の処理");
JavaScript

このときの流れをざっくり言うと:

  1. JavaScript エンジンが “A” を出力(メインスレッド)
  2. setTimeout を呼ぶと、「3秒後にこの関数を実行して」と Web API に依頼して、メインスレッドの仕事は終了
  3. メインスレッドはすぐに次に進み、”C” を出力
  4. 3秒後、Web API が「終わったよ、このコールバック実行して」とタスクキューに追加
  5. イベントループが、メインスレッドが暇になったタイミングで、そのコールバックを実行
  6. “B” が出力される

この間ずっと JavaScript が待ち続けているわけではなく、
待つのはブラウザ側の仕組み(Web API)。
JavaScript 本体は「次の行」にとっとと進む
わけです。

ここが重要です。
「シングルスレッドだけど非同期」は、JavaScriptエンジン+ホスト環境(ブラウザ/Node)+イベントループの連携で実現している、という構造の理解が重要です。


マルチスレッドじゃないことの「メリット」と「トレードオフ」

メリット1:言語としての複雑さがずっと低くなる

マルチスレッドになると、

  • 2つのスレッドが同時に同じオブジェクトを触るかもしれない
  • 片方が書き換えている途中に、もう片方が読みに来るかもしれない
  • それを防ぐために「ロック」「ミューテックス」「セマフォ」などが必要になる

といった問題が出てきます。

シングルスレッドなら、

  • 常に「今動いているコードは1箇所だけ」
  • コールスタックも1本だけ
  • 共有データの同時アクセスをほぼ心配しなくていい

というシンプルさがあります。

初心者にとってはこれは大きなメリットで、
「同期と非同期を理解すれば、スレッドやロックは一旦忘れても進める」 という状況を作ってくれます。

メリット2:DOM 操作などの UI 更新が直感的

ブラウザで DOM を操作するとき、

document.querySelector("#msg").textContent = "変更";
JavaScript

というコードがあったとして、
これを複数スレッドが同時に触ったらどうなるか?
…と考えなくていいのは、シングルスレッドだからです。

「今この瞬間、DOM を書き換えているのは世界に1箇所だけ」という前提で設計できるので、
ブラウザ側の実装も、あなたのコードもシンプルになります。

トレードオフ:重い同期処理に弱い(だから非同期が大事)

もちろん、デメリットもあります。

1本しかないメインスレッドを、長時間占有するような処理を書くと、

  • その間、クリックやスクロールに反応しなくなる
  • アニメーションがカクつく、固まる

という「ブロッキング」の問題がすぐに表面化します。

だから JavaScript では、

  • ネットワーク処理は非同期
  • ファイル I/O(Node.js)は非同期
  • タイマーも非同期

という設計が基本になっていて、
重い処理をできるだけメインスレッドから逃がす・分割する という発想がとても重要になります。


「じゃあマルチスレッドは一切ないの?」という疑問

Web Workers / Worker Threads という「別スレッド」は存在する

「シングルスレッド」とはいっても、
ブラウザや Node.js には、別スレッドを使う機能もあります。

  • ブラウザ:Web Workers
  • Node.js:Worker Threads

これらは「別スレッドで JavaScript を動かす」仕組みですが、
メインスレッドとはメモリ空間を共有しない(基本はメッセージでやりとり) という制約があります。

つまり、

  • メインの JavaScript 実行モデル(DOM さわるところ)はシングルスレッドを維持
  • 重い計算などを Worker に投げて、メインスレッドのブロッキングを避ける

という「折衷案」のような世界観になっています。

メインスレッドはあくまで1本、というのが大原則

重要なのは、

  • メインの JS 実行(イベントハンドラ、UI 更新など)は1本のスレッド
  • Worker は「別プロセス的な立場」で、メッセージ経由で協力する

という構造が変わっていないことです。

これにより、

  • UI 更新の一貫性
  • DOM 操作の安全性
  • 言語仕様としてのシンプルさ

を保ったまま、「どうしても必要なところだけ別スレッドの力を借りる」ことができます。


例題で感覚を固める:シングルスレッドだからこうなる

例1:関数の呼び出し順は必ず「一列」

function first() {
  console.log("first start");
  second();
  console.log("first end");
}

function second() {
  console.log("second start");
  third();
  console.log("second end");
}

function third() {
  console.log("third");
}

first();
JavaScript

出力は必ずこの順になります。

  1. first start
  2. second start
  3. third
  4. second end
  5. first end

途中に他の関数が割り込んだり、並行して third が動いたりはしません。

これは「コールスタックが1本しかない」=シングルスレッドの直接的な結果です。

例2:重い while があると、そのあいだイベントハンドラは動けない

const btn = document.querySelector("#btn");

btn.addEventListener("click", () => {
  console.log("クリック!");

  const start = Date.now();
  while (Date.now() - start < 3000) {
    // 3秒間ブロッキング
  }

  console.log("3秒後に終わった");
});
JavaScript

このボタンを連打しても、
1回目のクリックの while が終わるまで、2回目以降のクリックイベントは処理されません。

  • イベントはキューに溜まる
  • メインスレッドが暇になったら順に処理される

という形になるので、メインスレッドを長時間塞ぐと「後続のイベント」が全部遅れます。


まとめ:JavaScript がシングルスレッドである理由を一文で

最後に、ぎゅっとまとめるとこうです。

JavaScript は、ブラウザ上の安全でシンプルな UI スクリプトとして生まれたため、「DOM や共有状態を複数スレッドで触る複雑さ」を避け、1 本のメインスレッドで動く設計が採用された。
その代わり、イベントループと非同期 API を駆使することで、「シングルスレッドなのにノンブロッキング」という実行モデルを実現している。

あなたがこれから学ぶ

  • イベントループ
  • コールスタック
  • タスクキュー
  • Promise / async/await

といった概念は、全部この

「1本しかないメインスレッドを、いかに詰まらせずにうまく回すか」

というテーマにつながっています。

この「JavaScript はあえてシングルスレッドを選んでいる」という土台が腑に落ちると、
非同期処理の世界全体がだいぶクリアに見えてくるはずです。

タイトルとURLをコピーしました