非同期処理、なんとなく「なんか処理の終了を待機せずに進んでいくものらしい」くらいの認識でいませんか???
僕は鳥頭でちょっとでも複雑なものは理解するにものすごく時間がかかってしまうので、長年その程度の理解でいました。
なんとなく頭のモヤモヤがとれないままで、非同期、promise、Task、async / awaitっていう言葉に遭遇するだけで「うっ…」となっていました。
そんな僕でも、今はある程度「非同期処理がどんなものなのか」。何が「非同期」なのか。実際システム的に何が起こっているのかがイメージ出来るようになっています。
今回は、過去の僕のように非同期の概念の理解やイメージがあまりうまくできていない方向けに、他では見かけないような切り口で、易しく解説します!
そもそも非同期処理って何?
プログラムの基本は、上から順に1行ずつ処理していきます。
1行処理して、完了したらその次の処理に進む。これを繰り返すのが「同期処理」です。
対して「非同期処理」は、少し特殊な動きをします。
「処理が終わる前に次の行に進んでしまうもの」と説明されることもありますが、この考え方は今では誤解を招くかもしれません。
昔は実際に「その行の処理が完了する前に次の行に進む」という言葉通りの動きをしていましたが、現代の書き方(async/await)では、その行でちゃんと止まって完了を待っています。
「じゃあ同期処理と何が違うの??」と以前は僕も混乱していました。
一言で言うと、「どちらもその行の処理の完了が終わるまで待機するけど、待機中の柔軟さが違う」という感じになります。
弁当屋で例える同期処理と非同期処理
街のお弁当屋さんをイメージしてみてください。
このお弁当屋さんは、注文を受けると厨房で出来立てのお弁当を作ってくれます。
あなたの前のお客さんがレジの店員さんにお弁当を注文し、レジから厨房へオーダーが伝わり調理が開始されました。
あなたの前のお客さんは店員さんから番号札(出来たお弁当の引換券)を受け取って列から逸れました。
この後に起こることを、レジの店員さんが同期処理、非同期処理だった場合でそれぞれ考えてみます。
同期処理のお弁当屋さん
前のお客さんが注文を終えたので次はあなたの番です。
あなたはレジの店員さんに注文を伝えようとしますが、レジの店員さんはぼ~っとして動きません。
注文をしようとしても「前のお客さんのお弁当を作ってるのでお待ち下さい」と言ってきます。
あなたからすると「いやいや、注文は受け付けちゃって自分の弁当もオーダー通してよ!」って思いますよね?
実際のプログラムでも、時間がかかる処理を同期処理で書くと、その処理が終わるまでアプリがフリーズして動かなくなってしまうんです。
非同期処理のお弁当屋さん
非同期処理のお弁当やさんの場合は違います。
前のお客さんの注文が終わってオーダーが厨房に通ると、レジの店員さんは速やかにあなたの注文に応えられるようになります。
場合によってはあなたの注文の前に、別のお客さんの出来上がった弁当を袋に入れてお客さんに手渡したりと、その時に必要なことをテキパキと対応できるようになります。
こうして色々な仕事をこなしつつも、前のお客さんのお弁当が出来上がったら、適切なタイミングでそちらのタスクに戻ることも出来ます。
過去の注文の進行状況を待ちつつも、他の仕事も適宜対応できる。これが非同期処理を理解するためのポイントです。
ここでいう「他の仕事」というのが何なのかイマイチイメージできないという方は後で詳しく説明していますので、もう少し読み進めてみてくださいね。
なんでわざわざ非同期処理が必要なの?
非同期処理は有効活用すべきものですが、「いつでも使うもの」ではありません。
非同期処理が必要になるのは以下のような処理です(一例)
- 画面(UI)からAPIを呼び出すとき
- 大量のデータをDBから読み出してくるとき
- ファイルへのアクセスをするとき
こういう時間がかかる処理って、同期処理(その処理が完了するまで次の行に進まない処理)でやると、その処理の間、アプリが止まってしまうんです。
この「止まってしまう」っていうのは具体的に何がどう止まるんでしょうね?
同期処理だと止まるってのは何がどう止まるの?
例えば、とあるWebアプリの画面(UI)でボタンを押すと、APIを呼び出す処理を考えてみましょう!
画面側(フロントエンド)のソースコードが以下のようになっている場合で考えてみます。
// ボタンをクリックした時の処理
function onClickButton() {
console.log("ボタンが押されました");
// 【ここが重い処理!】APIを呼び出してデータを取ってくる
const data = getDataSync("/api/user-data");
// 取ってきたデータの表示処理
}6行目のgetDataSyncメソッドはAPIを実行し、このAPIはDBから大量のデータを取得してくるものなので、完了までに5秒ほどかかるとします。
↑のonClickButtonがこのまま実行されると、getDataSyncメソッドが呼ばれてから5秒ほど、Webアプリが停止してしまうんです。
ここで、「停止」というのが何を表しているのか考えてみましょう。
停止といっても、なにもPCがフリーズするわけではありません。
マウスやキーボードは勿論反応しますし、タスクバーから別のアプリケーション(メモ帳とかエクセルとか)に切り替えることも出来ますし、Webアプリを開いているブラウザ(Chrome)のタブ切り替えやメニューバーの操作も可能です。
止まってしまうのはあくまでWebアプリ。もっというと、そのWebアプリの画面の動きの世話をしているJavascriptです。
僕らが書いたJavaScriptのソースコードは、Chromeなどのブラウザに内蔵されている「JavaScriptエンジン」というプログラムが読み取って、画面に色々な動きを与えてくれています。
このエンジン、実は「一度に一つのことしかできない(シングルスレッド)」という単純な子です。そのため、同期処理で「5秒かかる作業」を命じると、その間エンジンは完全に付きっきりになり、他の作業をすべて放り出してしまいます。
JavaScriptエンジンは、ボタンのクリック監視やアニメーションの描画も担当しているため、この子がフリーズすると画面上のすべてが反応しなくなります。
この状態が長く続くと、ブラウザでは以下のような「ページが応答しません」のポップアップが表示されることがあります。

非同期処理にするとどうなるのか
では、さっき同期処理になっていた部分を非同期処理にしてみましょう。
// ボタンをクリックした時の処理
async function onClickButton() {
console.log("ボタンが押されました");
// API呼び出しを非同期に変更したもの
const data = await fetchDataAsync("/api/user-data");
// 取ってきたデータの表示処理
}6行目のAPI呼び出しにawaitが付きました。
ここで大事なのは、「JavaScriptエンジン(店員さん)」の動きです。
awaitの行に来ると、エンジンは厨房(APIサーバー)にオーダーを通し、「引換券」を発行します。- エンジンはその場で弁当の完了(APIレスポンスの到着)を待ちつつ、別の仕事を探します
- その間、エンジンは「画面のスクロール」や「ボタンの反応」といった別の仕事(UIの仕事)をテキパキとこなします。
つまり、コード上の「次の行(7行目以降)」には進みませんが、「エンジンくん自身」は他の場所へ仕事をしに行けるようになるんです。これが「止まらない」の正体です。
非同期処理の結果が返ってきたらどうなるの?
JavaScriptエンジンくんが非同期処理を待ちながら別のイベントを捌いている最中に、APIの結果が「お待たせ!」と返ってきたらどうなるのでしょうか?
今やってる別の仕事とごっちゃになりそうですが、そのへんはJavaScriptエンジンは「今やっているキリの良いところまで終わらせてから、非同期の結果を受け取りにいく」という動きをしてくれます。
弁当屋のレジ担当のスタッフの動きで考えてみましょう。
- あなたがお弁当を注文する
- レジ担当から厨房へオーダーを伝える
- 次のお客さんが弁当を注文中
このタイミングで厨房から「お弁当できたよ!」と言われても、レジ担当者は眼の前のお客さんとのやり取りを即刻中断してあなたにお弁当を渡してくれるわけではありませんよね。
今眼の前のお客さんとのやり取りが一段落したらお弁当を渡してきてくれるはず。それと同じです。
これはJavaScriptのイベントループという仕組みです。
JavaScriptエンジンくんは、「メインの仕事が空っぽになった瞬間」にだけ、タスクキューを見に行きます。
「お、お弁当ができてるな。次はこれを渡す処理をしよう」と、一つずつ順番に片付けていくわけです。
PromiseとかTaskは引換券
非同期処理を書くとき、必ずセットで登場するのが Promise や Task という言葉です。 JavaScriptならPromise、C#ならTaskと呼ばれますが、呼び名が違うだけで役割はほとんど同じ。
これらは一言でいうと、「未来に受け取る結果を約束するための『引換券』」のことなんです。
先ほどのお弁当屋さんの例に戻りましょう。
あなたが注文を済ませると、厨房で調理が始まります。お弁当はすぐには出来ないので、代わりに店員さんは「番号が書かれた引換券」をくれますよね。
この「お弁当そのものではないけれど、後でお弁当と交換できる券」。これがPromiseやTaskの正体です。
この引換券は、状況に応じて3つの状態に変化します。
待機中(Pending): お弁当を絶賛調理中。
完了(Fulfilled / Resolved): お弁当が完成!予約券をお弁当と交換できる状態。
失敗(Rejected): ごめん、トラブルで弁当作れなかった!というエラー状態。
引換券が「完了」になると、店員さんの仕事リスト(タスクキュー)に「お弁当を渡す」というタスクが追加されます。
店員さんはキリが良いところでそのリストを見て、あなたにお弁当を渡しに来ます
ここで改めて、先程の非同期処理のAPI呼び出し部分のコードを見てみましょう。
const data = await fetchDataAsync("/api/user-data");fetchDataAsync() は、実行した瞬間に「データを入れるための引換券(Promise)」を返してきます。もちろんこのタイミングではまだデータ取得処理は終わっていないので、引換券だけです。
そして、その前にある await は、JavaScriptエンジンに対して次のような指示を出しています。
「この引換券が『完了』になるまで待ってて。完了したら、引換券と交換で結果データを渡すからね。」
大事なことなので繰り返しになりますが、JavaScriptエンジンは「待ってて」と言われてはいるものの、非同期処理の場合は別の作業を始めています。
完了になり次第あらためてデータ(お弁当の中身)を取りに来ることになります。
PromiseやTaskについてもう少し詳しく
「引換券」というイメージがわいたところで、実際のコードの動きを整理してみましょう。
言語による「引換券」の呼び方
非同期処理で返ってくるこの引換券は、言語によって名前が違います。
- JavaScript: Promise(プロミス)
- C#: Task(タスク)
C#の場合は、非同期で呼び出すメソッドは定義するときに「Taskを返しますよ」とはっきり書くルール(型定義)があるので、「あ、このメソッドは結果ではなく引換券を返すんだな」と気づきやすいです。
対してJavaScriptは、メソッド宣言時に引数としてPromiseと指定する訳でもないのでちょっとイメージしづらいかもしれません。
JavaScriptはC#と違って空気を読みすぎてしまうため、メソッド宣言時にasyncと書くだけで自動的に戻り値がPromise(引換券)に包まれる仕組みなので、慣れないうちは「このメソッドは引換券ではなくAPIの実行結果をそのまま返してくれるんだ」と、勘違いが起こりがちです。
非同期呼び出し箇所の動作イメージ
改めて、先ほどのコードを見てみましょう。
const data = await fetchDataAsync("/api/user-data");await fetchDataAsync(“/api/user-data”)
の部分は、書き換えると
await 引換券
となります。
更に言い換えると「引換券が”完了”になるまで待つ。(ただし待ってる間他のUIの仕事もする)」ということになるわけです。
まとめ 非同期処理と仲良くなるために
非同期という言葉、僕は長年「なんだかよくわからないもの」という理解で止まっていましたが、その正体は「お弁当ができるまで、レジを止めずに他のお客さんを捌く」という、現実世界でもよく見かけるものでした。
- Promise / Task は単なる「引換券」
- await は「引換券の中身が出来上がるまでテキトーに他の事しながら待ってて」という合図
- async は「この関数は引換券を発行するよ」という目印
こういう理解をすれば、だいぶ非同期処理がイメージしやすくなると思います。
また、JavaScriptエンジンというものを絡めて非同期処理について理解することで、更に理解を深めることも出来ました。
僕のように非同期処理がイメージできず困っている人が、この記事で少しでも理解を深めてもらえると嬉しいです!

コメント