プログラムの処理の進み方には大きく同期処理と非同期処理があります。
両者の違いは性能や設計、ユーザー体験に直結します。
本記事では、初学者がつまずきやすい用語や誤解を避けながら、基本概念からイベントループの動き、設計判断、実践の注意点までを丁寧に解説します。
同期処理と非同期処理の違い

同期処理とは
定義
同期処理とは、ある処理が終わるまで次の処理を開始しない進め方です。
1列で順番に実行され、上から下へと流れるコードの見た目どおりに動作します。
メソッドの戻り値は即時に得られ、呼び出し元は結果が返るまで待機します。
生活の比喩
レジに1つ並んで、前の人の会計が終わるまで自分は進めない状態です。
順番が明確でわかりやすい反面、待ち時間が長くなることがあります。
簡単なコード例
同期処理は読みやすさが特長です。
例えばファイルを読み、続けて送信する処理は次のように直列に書けます。
内容 = ファイルを読む() // 終わるまで待つ
結果 = サーバーに送る(内容) // 終わるまで待つ
表示(結果)
非同期処理とは
定義
非同期処理とは、時間のかかる処理を開始したら待たずに先へ進み、完了したタイミングで結果を受け取る進め方です。
呼び出しは即時に返り、結果はコールバックやPromise/Futureなどを通じて後で受け取ります。
生活の比喩
フードコートで番号札を受け取り、席で別の作業をしながら呼び出しを待つイメージです。
待っている間に他のことができます。
簡単なコード例
非同期処理では「開始」と「結果の受け取り」が分離します。
約束 = サーバーに送る_非同期(内容) // すぐ返る
他の作業をする()
約束.完了したら(結果 => 表示(結果))
ブロッキングとノンブロッキング
用語の違い
- ブロッキング(blocking): 呼び出し元の実行を止める動作を指します。完了までスレッドや処理は待機します。
- ノンブロッキング(non-blocking): 呼び出し元の実行を止めません。呼び出しはすぐ返り、完了通知は別の仕組みで行います。
同期/非同期とブロッキング/ノンブロッキングは本来は別の軸です。
しばしば同一視されますが、厳密には以下のように組み合わせが存在します。
組み合わせの例
区分 | 説明 | 典型例 |
---|---|---|
同期×ブロッキング | 終わるまで待つ | ファイル読み込みAPIが戻り値で内容を返す |
同期×ノンブロッキング | すぐ返すが現在の状態のみ返す | ロックのtryLock()が成功/失敗だけ即返す |
非同期×ノンブロッキング | すぐ返し、完了は後で通知 | HTTPリクエストがPromise/Futureを返す |
非同期×ブロッキング | 非同期開始後に明示的に待つ | Future.get()で完了まで待機する |
実務では「非同期=ノンブロッキング」が多く、「同期=ブロッキング」が一般的です。
ただしAPI設計や使い方次第で上記のとおり多様な組み合わせが生じます。
よくある混同
非同期APIを使っても、完了待ちをすぐに呼ぶと実質的に同期ブロッキングと同じになります。
設計の意図に沿って、どこで待つか(あるいは待たないか)を明確にすることが重要です。
非同期は並列ではない
並行性と並列性の違い
- 並行(concurrency): 論理的に同時進行に見えること。順番を素早く切り替えながら複数の仕事を進めます。
- 並列(parallelism): 物理的に同時に走ること。複数のCPUコアやスレッドで本当に同時に実行します。
非同期は「待ち時間に別の処理へ切り替える」仕組みであり、必ずしも並列に実行されるわけではありません。
例: シングルスレッドのイベントループ
ブラウザやNode.jsの多くの処理は1本のスレッドでイベントループにより非同期実行されます。
見た目は同時進行でも、厳密には1つずつ順番に処理しています。
並列化が必要なとき
CPUバウンドな重い計算は非同期(I/O待ちの効率化)だけでは高速化されません。
スレッドプールやプロセス分割、GPUなどによる並列化が有効です。
非同期処理の仕組みと動き

コールバックとPromise/Future
コールバック
非同期処理に関数を渡し、完了時に呼び返してもらう方式です。
軽量ですが、入れ子が深くなりやすく、エラーやキャンセル処理が複雑化します(いわゆるコールバック地獄)。
Promise/Future
結果を後から受け取る「箱」を返す方式です。
Promise/Futureは未完了(待機中)の状態を持ち、完了や失敗で状態が更新されます。
完了後にthen/awaitで結果を取り出せるため、コールバックの入れ子を減らせます。
言語例として、JavaScriptのPromise、JavaのCompletableFuture、Pythonのconcurrent.futures.Futureなどがあります。
使い分け
- 簡単な一回限りの通知はコールバックでも十分です。
- 複数の非同期の合成、タイムアウトやキャンセル、例外伝播を扱うならPromise/Futureのほうが記述が安定します。
async/awaitの基本
仕組み
async/awaitはPromise/Futureを見通しよく書くための構文糖衣です。
awaitは「ここで結果が必要なので完了まで待ってもよい」という意思表示ですが、スレッドをブロックせず、制御をイベントループに返して他の処理を進められる点が重要です。
例
JavaScript風の例です。
async function main() {
const user = await fetchUser(); // 完了まで他のタスクに制御を譲る
const posts = await fetchPosts(user.id);
render(posts);
}
注意点
- awaitはasync関数の内部でのみ使用できます。
- ループ内の順次awaitは直列実行になります。独立なタスクは先にまとめて開始し、後で待つと効率的です。
// 非効率: 逐次待機
for (const id of ids) {
const item = await fetchItem(id);
consume(item);
}
// 効率的: 先に全て開始し、まとめて待機
const promises = ids.map(id => fetchItem(id));
const items = await Promise.all(promises);
items.forEach(consume);
イベントループとタスクキュー
基本サイクル
イベントループは「タスクキューから1つ取り出す→実行→完了したら次へ」を高速に繰り返します。
I/Oが終わると完了イベントがキューに積まれ、空き時間に順次処理されます。
マイクロタスクとマクロタスク
一部の環境(JavaScriptなど)には優先度の異なるキューがあります。
マイクロタスク(Promiseのthenなど)はマクロタスク(setTimeoutやI/O完了イベントなど)より先に実行されます。
これにより、細かな後処理が迅速に行われ、UIの応答性や整合性が保たれます。
タイマーとI/Oの扱い
setTimeout(0)は「できるだけ早く」という意味であり「今すぐ」ではありません。
現在のタスクが終わってからキューに並び、他のイベントとの順番に従って実行されます。
I/O完了も同様に、完了イベントの発火順序とキューの優先度に従います。
I/OバウンドとCPUバウンドの選び方
見分け方
- I/Oバウンド: ネットワークやディスク待ちが支配的。CPUの待機時間が長い。
- CPUバウンド: 計算や圧縮、画像処理などCPU時間が支配的。待ち時間は少ない。
戦略
- I/Oバウンドには非同期処理が有効です。待ち時間を利用して他の仕事を進め、スループットを向上させます。
- CPUバウンドには並列化(マルチスレッド/マルチプロセス)が必要です。非同期だけでは根本的な改善はしにくいです。
具体例
- データベース照会やWeb API呼び出しは非同期I/Oで効率化できます。
- 画像サムネイルの大量生成はワーカー(スレッド/プロセス)に分散し、キューで調整します。
メリット・デメリットと選び方
同期処理の利点と弱点
利点
同期処理はコードの流れが素直で理解しやすく、デバッグも容易です。
ステップ実行で状態を追跡しやすく、順序依存のロジック(トランザクションの段階的処理など)が書きやすいです。
弱点
I/O待ちでCPUを遊ばせやすく、スループットや応答性が低下します。
UIスレッドでブロックすると画面が固まるなどユーザー体験が悪化します。
非同期処理の利点と弱点
利点
待ち時間を有効活用できるため、少ないスレッドで高い同時実行数を捌けます。
サーバーでは接続あたりのリソース消費を抑えやすく、クライアントではUIの応答性を保ちやすいです。
弱点
制御の流れが分散し、可読性やテスト難易度が上がりやすいです。
順序保証、タイムアウト、キャンセル、エラーハンドリングの設計が不可欠で、適切に行わないとバグが潜みます。
初心者向けの判断基準
まずは同期で正しさを確立
最初は同期で動く正しい実装を作り、要件を満たすことを優先します。
実際の遅さやボトルネックを計測してから非同期化すると、設計が安定します。
非同期へ切り替える目安
- ネットワークやディスク待ちが原因で処理が詰まるとき
- UIの応答性が重要で、操作中に固まると困るとき
- 多数の軽量リクエストを効率よく捌きたいとき
設計時のポイント
- 非同期で「どこまで並行させるか」を決め(上限も決める)、完了を「どこで待つか」を明確化します。
- タイムアウト、リトライ、キャンセルとエラー方針を最初に定義します。
- ログやトレースの方針を用意し、観測可能性を確保します。
プログラミング実践ベストプラクティス
タイムアウト・リトライ・キャンセル
タイムアウト設計
外部I/Oには明示的なタイムアウトを必ず設定します。
サービスごとに期待応答時間と最大許容時間を決め、上位の処理には全体のデッドラインを伝播します。
タイムアウト値は実測に基づき、環境や負荷で調整できるよう設定化します。
リトライ設計
一時的な失敗(ネットワークの瞬断など)は指数バックオフ+ジッタでリトライします。
副作用がある操作は冪等化(idempotency)を検討し、最大試行回数と合計待ち時間の上限を設けます。
恒久的エラー(4xxなど)は即時に失敗を返し、無駄な再試行を避けます。
キャンセル伝播
ユーザー操作の取り消しや上位タイムアウト時には、下位の非同期処理へキャンセル信号を伝播します。
キャンセル用トークン(例: AbortControllerやCancellationToken)をAPI境界で受け渡し、処理側は定期的にトークンを確認して早期終了します。
キャンセル時は部分結果やリソースを確実に後始末します。
エラーハンドリングと例外設計
エラーの分類と扱い
- 一時的(リトライ可能)か恒久的(リトライ不可)かを区別します。
- 業務ドメインのエラーと技術的エラーを分け、メッセージとコードを揃えます。
- タイムアウト/キャンセルは明示的な型やコードで表現します。
例外と非同期エラー
awaitではtry/catchで同期風に扱えますが、PromiseやFutureの合成時には集約エラーや部分成功を考慮します。
複数の並行タスクでは全体を落とす条件と継続可能な条件を定義し、finallyで後始末を徹底します。
未処理の拒否(unhandled rejection)はプロセス全体に影響するため、グローバルハンドラで監視します。
失敗の可観測性
エラーはログに加え、メトリクス(失敗率、レイテンシ、タイムアウト数)とトレース(相関ID、スパン)で可視化します。
非同期では呼び出し元と実行箇所が離れるため、相関IDをコンテキストに保持し、スレッドやコルーチン越しに伝搬する仕組みを整えます。
可読性とデバッグのコツ
構造化のテクニック
- ネストを避け、早期returnと小さな関数で流れを平坦にします。
- 非同期の境界(開始、待機、タイムアウト、キャンセル)を関数として明確に分離します。
- 名前に動作を刻みます(例: fetchWithTimeout、retryingSend)。
ログとトレース
開始時、完了時、失敗時で最低限のログを一貫したフォーマットで記録します。
非同期では順序が前後するため、時刻だけでなく相関IDや軽量なステータスを必ず付与します。
パフォーマンス観点
イベントループをブロックする重いCPU処理はワーカーに逃がします。
独立タスクはまとめて開始し、Promise.allや等価の手段で待つと効率的です。
ただし同時数は無制限にしないで、セマフォやキューで最大並行数を制御します。
よくあるアンチパターン
- 非同期APIを呼んですぐに同期待機する(メリットを打ち消します)。
- ループ内でawaitして大量の逐次I/Oにしてしまう。
- 例外とキャンセルをcatchしないまま握りつぶす。
- タイムアウト未設定で永久待機する。
まとめ
同期処理は読みやすく正確性を担保しやすい一方、待ち時間に弱く、応答性やスループットで不利になりがちです。
非同期処理は待ち時間を有効活用して効率を高めますが、制御の流れやエラー処理が複雑になります。
非同期は並列そのものではなく、I/Oバウンドの効率化に強みがある点を押さえてください。
まずは同期で正しさを確立し、計測に基づいて非同期化するのが安全です。
その際は、タイムアウト、リトライ、キャンセル、エラーハンドリング、可観測性の設計を最初に決めることが成功の近道です。
理解と実践を行き来しながら、自分のプロジェクトに最適なバランスを見つけていきましょう。