この記事は「女性エンジニア応援」Advent Calendar 2025 8日目の記事です。
はじめに
同期とか非同期って、プログラミングを勉強すると割と最初の方にぶち当たるちょっとした壁かなと思っているのですが、最初からちゃんと中身を理解するのって難しいですよね。
私自身エンジニアになって半年経っても、なんとなく「時間のかかる処理を並行してやってる」くらいのイメージしかなく、ずっと曖昧なまま使っていたので改めて仕組みなどについて調べようと思いました。
本記事ではkotlinを使ってコード例を出したり説明をするので、処理の仕方の理解などが他の言語だと異なる場合もあるのでその点ご留意ください。
TL;DR
- 同期処理はスレッドがブロックされるので1つの処理が終わるまで次の処理は始まらない
- 非同期処理は待ち時間が発生した際にスレッドをブロックしないため並行して別の処理が始まる
対象読者
- 同期処理と非同期処理についてよくわかっていない方
- プログラミング始めたばかりの方
そもそもCPUとは?
非同期とか同期とかを理解する前に、そもそも私はCPUやスレッドなど基礎的なことがよくわかっていませんでした。
CPUはデータの制御や計算を行う頭脳のような部分です。
CPUの性能はコア数やスレッド数などで変わります。
コアは実際に処理を行う部分で、スレッドは処理の単位を表します。
例えば1コアなら一度に実行できるスレッドは1つで、1コア2スレッドの場合は2つのスレッドを高速に切り替えながら同時に実行しているように見せています。
並行処理と並列処理
冒頭で並行という言葉を使いましたが、私はこの言葉の意味を曖昧なまま使っていて並行と並列の違いがよくわかっていませんでした。
並行(concurrent)
同時に複数の処理がwip状態であること
1人の人間がマルチタスクをしている状態と同じで、複数の作業を切り替えながら同時実行しているといった具合です。
並列(parallel)
複数の処理を物理的に同時に実行すること
複数の人間が同時に別々の作業をしている状態と同じで、複数のスレッドが別のCPUコアで同時実行されています。
並行は1つのコアが複数のスレッドを切り替えながら実行して、同時実行しているように見せかけているだけで、並列は実際に複数のスレッドを複数のコアが同時に実行しているという決定的な違いがあります。

同期処理と非同期処理
ようやく本題に入ります。
同期処理
fun task(name: String) {
println("$name start")
Thread.sleep(1000)
println("$name end")
}
fun main() {
task("A")
task("B")
}
// 実行結果
// A start
// A end
// B start
// B end
task関数はスレッドを1000msスリープさせるものですが、その間はスレッドがブロックされるためAの処理が完全に終わってからBの処理が開始されていることがわかります。
非同期処理
suspend fun task(name: String) {
println("$name start")
delay(1000)
println("$name end")
}
fun main() = runBlocking {
val task1 = launch { task(("A")) }
val task2 = launch { task(("B")) }
task1.join()
task2.join()
}
// 実行結果
// A start
// B start
// A end
// B end
task関数はdelayを使って1000msスリープさせていますが、その間スレッドはブロックされないためAの処理の待ち時間にBの処理が開始されていることがわかります。
非同期だと何が嬉しいか?
1つの処理を実行中に待ち時間が発生した場合、CPUは基本暇になります。
ただ待っているだけということです。効率が悪い感じがしますね。
効率厨の私ならちょっとでも何もしない時間が発生するなら、他にできることを並行してやろうという気持ちになります。
そういう時に最適なのが非同期処理です。
どういうときに使う?
時間のかかる処理ならなんでも非同期にすればいいというものでもありません。
例えば以下のような、
suspend fun countUp(name: String) {
println("$name start")
var count = 0
for (i in 0 until 1_000_000_000) count += i
println("$name end")
}
fun main() = runBlocking {
val task1 = launch { countUp(("A")) }
val task2 = launch { countUp(("B")) }
task1.join()
task2.join()
}
// 実行結果
// A start
// A end
// B start
// B end
for文で10億回繰り返すような処理だと、確かに通常より時間はかかりますが待ち時間は発生しないため非同期処理にしても並行処理にはなりません。
非同期処理を使う上で大事なキーワードは「停止」と「再開」です。
単純な計算ではなく、API通信やDB操作などといった待ち時間が発生した際にその処理を一時停止して、通信などが完了した際に再開できるようなものが非同期処理には適しています。
非同期処理の具体的な動き
kotlinでは非同期処理を実行する環境をコルーチンと呼び、非同期なメソッドをsuspend関数として定義します。
suspend関数は自身のタイミングでsuspend(停止)とresume(再開)が可能で、停止している間はスレッドを別のコルーチンに譲っています。
別のコルーチンに切り替わる条件としては
- 非同期のIO(API通信、DB関連、ファイル読み書きなど)
- delay()
- withContext
などがあります。逆に単に長時間繰り返すだけの関数などでは待ち時間がないため切り替えは発生しません。
まとめ
時間がかかる処理を並行して同時にやってくれる処理が非同期処理、みたいなふわっとした理解だったのが、今回ちゃんと調べた結果仕組みまで理解できました。
非同期処理は(1コアの場合)本当に同時に処理を実行しているわけではなく、同じスレッドで処理を切り替えているだけなので、1人の人間が行うマルチタスクと同じなんだと思いました。
ただ、非同期処理の本質は「同時実行しているように見せること」ではなく待ち時間にスレッドをブロックしないことでCPUを有効活用することだと思います。
同じように理解が曖昧な方への助けになれば幸いです。