この記事は株式会社ドットログによる
コンストラク体操日記 Advent Calendar 2025 の 15日目 の記事です。
はじめに
同期・非同期って、最初めちゃくちゃ分かりづらいのに、入門のころから容赦なく出てきます。ここがふんわりのままだと、コードを書くのがだんだんしんどくなりがちです。
この記事では ラーメン屋のバイト をイメージしながら、
- 非同期・同期処理が「なぜあるのか」
-
async / awaitやPromiseが「どんなノリで使われるのか」 - 並列・並行の違い(※ここは +α)
を、できるだけ日常ことばで整理します。
この記事でわかること
- 非同期・同期処理が なぜ存在しているか、ざっくり仕組みから理解できる
-
async / awaitとPromiseを “意味を理解して” 書けるようになる - 「副作用」という考え方に軽く触れられる
- 並列処理・並行処理との関係も、ラーメン屋の例でふんわり掴める
(※並列処理の話は +アルファの補足 なので、軽く読むだけでも OK)
ラーメン屋で考える同期・非同期・並行・並列
まずはプログラムから離れて、ラーメン屋のバイトで考えます。
ラーメン屋の接客フロー
やることはざっくりこんな感じにします:
- お客様をテーブルに案内
- 注文を聞く
- ラーメンを作る
- ラーメンを配膳する
- お客様が食べ終わったことを確認する
- お会計をする
- お皿を洗う
新人店員 1人:完全「順番どおり」=同期処理
新人店員が 1 人だけいるとします。この人は「いまの作業が終わるまで次に進まない」タイプです。
「食べ終わったことを確認する」が終わっていないから、「会計」や「次のお客さんの案内」に進まない、という状態です。
1 個ずつ、完全に順番に進むイメージが 同期処理 です。
ここで一つ大事なのが、
プログラミングの「デフォルト」はこの同期処理
だということです。書いたコードは上から順番に、一つ終わったら次へが前提です。
非同期 は、この流れの中にある「待ち時間」をどう扱うか、という 追加ルール だと思っておくとラクです。
ベテラン店員 1人:待ち時間で別の作業を挟む=非同期 / 並行
次は店員は 1 人のまま、ベテラン店員に変えます。この人は、
- お客さんがラーメンを食べているあいだに空いた皿を洗ったり
- 次のお客さんを案内したり
- 水を足しに行ったり
します。
ただ待つのではなく、待ち時間に別の作業を進める感じです。これがここでの 非同期処理 のイメージです。
ただし、体は 1 つなので、「皿洗いしながら同時に注文を聞く(物理的に同時進行)」みたいな同時進行はできません。
実際にやっているのは、
- 皿洗いをする
- いったん手を止めて注文を取りに行く
- 終わったらまた皿洗いに戻る
というように、1 つずつを切り替えているだけです。
「並行処理」と「JavaScript の非同期」の関係
ここで「並行処理(concurrency)」という言葉を軽く出しておきます。
並行処理とは?
厳密にはこんな感じです:
並行処理(concurrency)
複数のタスクが「同時に進行しているように見える」状態。
必ずしも同じ瞬間に動いている必要はなく、「進行中のタスクが複数ある」ことがポイント。
店員の例だと、
- A テーブルの料理待ちのあいだに
- B テーブルの注文を取りに行き
- また A に戻る
というように、
「A と B の対応がどちらも進行中」
になっているので、これは並行処理といえます。
JavaScript の非同期はどうなっているの?
ここで JavaScript の話を少しだけ混ぜます。
-
JavaScript(ブラウザ・Node.js の基本実行環境)は シングルスレッド
- =「店員は 1 人だけ」という世界観
なのに、setTimeout や fetch で非同期っぽいことができるのは、実はこういう役割分担になっているからです:
-
店員(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 のコードにしてみます。やりたい流れは:
- 少し待つ → 「麺ゆで完了」
- また待つ → 「盛り付け完了」
- また待つ → 「配膳完了」
という 3 ステップです。
ここでは
- コールバック
Promise.thenasync / 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 全体が止まるわけではない
というのがポイントです。
裏では、
-
waitがPromiseを返していて -
awaitが「そのPromiseが終わったらこの続きを動かしてね」とイベントループにお願いしている
だけです。
見た目は
待つ → ログ → 待つ → ログ → 待つ → ログ
という同期処理に近く、でも中身はちゃんと非同期、というのが async / await のポイントです。
3 つの違いをざっくり表にすると
| 書き方 | ざっくり意味 |
|---|---|
| コールバック | 「終わったらこれお願い!」を 入れ子 で渡す |
| Promise.then | 「終わったら次これ」を 横にチェーン する |
| async / await | 同期っぽく順番で書ける Promise の構文糖 |
どれも 非同期 を扱っていますが、「どう書くか」「どれだけ読みやすいか」が段階的に進化していったイメージです。
おわりに
非同期処理は、一度つまずくと「とりあえず async 付けとけばいいか…」となりがちです。でも、ラーメン屋レベルの直感で
- どこに待ち時間があるのか
- その間に別の作業をしたいのか(= 非同期にする意味があるか)
- どこはあえて同期のままでいいのか(順番が超大事なところ)
を意識できると、async / await や Promise の使いどころがだんだん見えてきます。
自分のコードの中にいる店員さんをイメージしながら、
「ここは新人でいい」
「ここだけベテランモードにする」
「ここはキッチン(ブラウザや OS)に任せる」
と考えてみると、非同期への苦手意識が少し薄れるはずです 🙌