9
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

この記事は株式会社ドットログによる
コンストラク体操日記 Advent Calendar 2025 の 15日目 の記事です。

はじめに

同期・非同期って、最初めちゃくちゃ分かりづらいのに、入門のころから容赦なく出てきます。ここがふんわりのままだと、コードを書くのがだんだんしんどくなりがちです。

この記事では ラーメン屋のバイト をイメージしながら、

  • 非同期・同期処理が「なぜあるのか」
  • async / awaitPromise が「どんなノリで使われるのか」
  • 並列・並行の違い(※ここは +α)

を、できるだけ日常ことばで整理します。

この記事でわかること

  • 非同期・同期処理が なぜ存在しているか、ざっくり仕組みから理解できる
  • async / awaitPromise を “意味を理解して” 書けるようになる
  • 「副作用」という考え方に軽く触れられる
  • 並列処理・並行処理との関係も、ラーメン屋の例でふんわり掴める
    (※並列処理の話は +アルファの補足 なので、軽く読むだけでも OK)

ラーメン屋で考える同期・非同期・並行・並列

まずはプログラムから離れて、ラーメン屋のバイトで考えます。

ラーメン屋の接客フロー

やることはざっくりこんな感じにします:

  1. お客様をテーブルに案内
  2. 注文を聞く
  3. ラーメンを作る
  4. ラーメンを配膳する
  5. お客様が食べ終わったことを確認する
  6. お会計をする
  7. お皿を洗う

新人店員 1人:完全「順番どおり」=同期処理

新人店員が 1 人だけいるとします。この人は「いまの作業が終わるまで次に進まない」タイプです。
「食べ終わったことを確認する」が終わっていないから、「会計」や「次のお客さんの案内」に進まない、という状態です。
1 個ずつ、完全に順番に進むイメージが 同期処理 です。

ここで一つ大事なのが、

プログラミングの「デフォルト」はこの同期処理

だということです。書いたコードは上から順番に、一つ終わったら次へが前提です。

非同期 は、この流れの中にある「待ち時間」をどう扱うか、という 追加ルール だと思っておくとラクです。

ベテラン店員 1人:待ち時間で別の作業を挟む=非同期 / 並行

次は店員は 1 人のまま、ベテラン店員に変えます。この人は、

  • お客さんがラーメンを食べているあいだに空いた皿を洗ったり
  • 次のお客さんを案内したり
  • 水を足しに行ったり

します。

ただ待つのではなく、待ち時間に別の作業を進める感じです。これがここでの 非同期処理 のイメージです。

ただし、体は 1 つなので、「皿洗いしながら同時に注文を聞く(物理的に同時進行)」みたいな同時進行はできません。

実際にやっているのは、

  • 皿洗いをする
  • いったん手を止めて注文を取りに行く
  • 終わったらまた皿洗いに戻る

というように、1 つずつを切り替えているだけです。

「並行処理」と「JavaScript の非同期」の関係

ここで「並行処理(concurrency)」という言葉を軽く出しておきます。

並行処理とは?

厳密にはこんな感じです:

並行処理(concurrency)
複数のタスクが「同時に進行しているように見える」状態。
必ずしも同じ瞬間に動いている必要はなく、「進行中のタスクが複数ある」ことがポイント。

店員の例だと、

  • A テーブルの料理待ちのあいだに
  • B テーブルの注文を取りに行き
  • また A に戻る

というように、

「A と B の対応がどちらも進行中」

になっているので、これは並行処理といえます。

JavaScript の非同期はどうなっているの?

ここで JavaScript の話を少しだけ混ぜます。

  • JavaScript(ブラウザ・Node.js の基本実行環境)は シングルスレッド

    • =「店員は 1 人だけ」という世界観

なのに、setTimeoutfetch で非同期っぽいことができるのは、実はこういう役割分担になっているからです:

  • 店員(JavaScript 本体のスレッド)

    • 「注文をメモして、キッチンに渡す」
    • 「終わったら呼んでね」とだけ頼む
  • キッチン(ブラウザ / Node.js の API や OS)

    • ラーメンを茹でる・ネットワーク通信をする・ファイルを読む…といった重い作業を担当
  • ベル(イベントループ)

    • キッチンが「終わったよ〜」と知らせたときに、店員に「次この処理やって」と渡す仕組み

JS が「待っているあいだに他のことをやっている」というより、
「待ちが必要な仕事はキッチンに任せて、終わったら呼んでもらう」
というイメージが実態に近いです。

なので、

  • 非同期だから並列になる わけではなく、
  • 店員は 1 人のまま、タスクの順番を上手くやりくりしている(並行)

と捉えると、変に誤解せずに済みます。

※厳密には Promise.resolve().then(...) など、実際には「待ち時間ゼロだけど、処理の順番だけ後ろに回す」タイプの非同期もあります。この記事では話をシンプルにするため、「時間がかかる処理(待ちがある処理)の非同期」 を中心に説明しています。

並列処理:店員を増やす(+α)

ここから先は、直感をふんわり広げるための おまけ です。

今度は、そもそも店員の人数を増やすことを考えます。
店員を 2 人以上にするイメージが、ここでいう 並列処理(parallelism) です。

新人 2 人:並列だけど中は同期

店員が 2 人になりましたが、どちらも新人です。
それぞれはさっきの新人と同じで、

「待ち時間はただ待つ」スタイル(= 各レーンの中身は同期)

ですが、お店としては 2 本のレーンがある 状態になります。

レーンは 2 本なので 並列 ですが、それぞれのレーンの中身は同期処理のままです。

ベテラン 2 人:並列かつ非同期

店員が 2 人いて、どちらもベテランだとどうなるか、というパターンも見ておきます。

ここまでをざっくりまとめると、

  • 並行処理(concurrency)

    • 1 人の店員(1 スレッド)がタスクを切り替えながら頑張る
  • 並列処理(parallelism)

    • そもそも店員(スレッド・プロセス)を増やす

というイメージになります。

JavaScript の世界では、基本的には「1 人のベテランが並行でタスクをさばいている」イメージで動いています。

本当の並列処理は、

  • Web Worker(ブラウザ)
  • Worker Threads / Cluster(Node.js)

など 別スレッド・別プロセス の仕組みで扱われることが多いです。

実務的には、多くのフロント・バックエンドコードは「非同期(並行)」さえ理解していれば十分です。

並列処理に強いメジャー言語 5 つ

言語 並列処理の特徴
C++ ネイティブスレッドで最速級。並列アルゴリズムも標準化。
Java Thread / ForkJoinPool で強力な並列。JVM の最適化が優秀。
C# Task Parallel Library(TPL)が並列最適化されている。
Go goroutine が OS スレッドにマッピングされ、コアを活用して並列。
Rust 所有権モデルで安全な並列。Rayon などのライブラリが強い。

JavaScript で見る「非同期の書き方 3 段階」

ラーメン屋の話を、ここからは JavaScript のコードにしてみます。やりたい流れは:

  1. 少し待つ → 「麺ゆで完了」
  2. また待つ → 「盛り付け完了」
  3. また待つ → 「配膳完了」

という 3 ステップです。

ここでは

  • コールバック
  • Promise.then
  • async / await

の 3 パターンを並べてみます。

A) コールバック(ネスト地獄)

setTimeout(() => {
  console.log("麺ゆで完了");
  setTimeout(() => {
    console.log("盛り付け完了");
    setTimeout(() => {
      console.log("配膳完了");
    }, 500);
  }, 500);
}, 500);

「◯ミリ秒経ったらこれしてね。そのあとさらに◯ミリ秒経ったら次これしてね…」という形で、
「後でやること」を入れ子にして渡していくスタイルです。

シンプルではありますが、ネストが深くなりやすく、処理が増えるほどコードが右に右に伸びていきます。
これがいわゆる コールバック地獄(入れ子地獄) です。

B) Promise.then(チェーン)

const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

wait(500)
  .then(() => {
    console.log("麺ゆで完了");
    return wait(500);
  })
  .then(() => {
    console.log("盛り付け完了");
    return wait(500);
  })
  .then(() => {
    console.log("配膳完了");
  });

wait は、

「ms ミリ秒経ったら resolve で『終わったよ』と合図してくれる関数」

です。ラーメン屋でいうと 「3 分経ったら鳴るアラーム」 みたいなものです。

.then()

「この作業が終わったら次にこれをやる」

という処理を 横一列に並べられる メソッドです。

A のように入れ子にせず、「終わったらこれ → さらに終わったらこれ」とつなげて書けるのがメリットです。

C) async / await(同期っぽく書ける非同期)

const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

async function serve() {
  await wait(500);
  console.log("麺ゆで完了");
  await wait(500);
  console.log("盛り付け完了");
  await wait(500);
  console.log("配膳完了");
}

serve();

一見すると「ただ順番どおりのコード」のように見えます。

  • await は「この処理が終わるまで この関数の続きを 後ろにずらす」
  • ここで止まるのは「その関数の続き」だけで、JavaScript 全体が止まるわけではない

というのがポイントです。

裏では、

  • waitPromise を返していて
  • await が「その Promise が終わったらこの続きを動かしてね」とイベントループにお願いしている

だけです。

見た目は

待つ → ログ → 待つ → ログ → 待つ → ログ

という同期処理に近く、でも中身はちゃんと非同期、というのが async / await のポイントです。

3 つの違いをざっくり表にすると

書き方 ざっくり意味
コールバック 「終わったらこれお願い!」を 入れ子 で渡す
Promise.then 「終わったら次これ」を 横にチェーン する
async / await 同期っぽく順番で書ける Promise の構文糖

どれも 非同期 を扱っていますが、「どう書くか」「どれだけ読みやすいか」が段階的に進化していったイメージです。

おわりに

非同期処理は、一度つまずくと「とりあえず async 付けとけばいいか…」となりがちです。でも、ラーメン屋レベルの直感で

  • どこに待ち時間があるのか
  • その間に別の作業をしたいのか(= 非同期にする意味があるか)
  • どこはあえて同期のままでいいのか(順番が超大事なところ)

を意識できると、async / awaitPromise の使いどころがだんだん見えてきます。

自分のコードの中にいる店員さんをイメージしながら、

「ここは新人でいい」
「ここだけベテランモードにする」
「ここはキッチン(ブラウザや OS)に任せる」

と考えてみると、非同期への苦手意識が少し薄れるはずです 🙌

9
2
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
9
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?