Kotlinのsuspend関数は、コルーチンの中で使う、一時停止と再開に対応した関数です。
ただし、最初に重要な点をはっきりさせておくと、suspendは
- 自動で並列実行してくれる仕組み
- 自動で別スレッドに逃がしてくれる仕組み
- どんな重い処理でも勝手に非ブロッキング化してくれる仕組み
ではありません。
suspendが表しているのは、あくまで「この関数はサスペンドポイントで中断し、あとで再開できる」という性質です。
suspend関数の基本
普通の関数はこう書きます。
fun greet(): String {
return "Hello"
}
suspend関数はこうです。
suspend fun fetchData(): String {
return "data"
}
見た目の違いはkotlin suspend が付いているだけですが、意味は大きく異なります。
このsuspendが付いた関数は、コルーチンや他のsuspend関数の中から呼び出せる、一時停止可能な関数になります。
「一時停止できる」とはどういう意味か
ここで注意したいのは、suspend関数はどの行でも自由に止まるわけではないということです。
より正確には、suspend関数はサスペンドポイントで中断・再開できる関数です。
代表的なサスペンドポイントには次のようなものがあります。
delay()withContext()- 他の
suspend関数の呼び出し
つまり、suspendが付いているからといって、関数本体の処理すべてが自動的に非ブロッキングになるわけではありません。
なぜsuspend関数が必要なのか
suspend関数は、特に待ち時間を含む処理を扱うときに役立ちます。
代表例は次の通りです。
- API通信
- データベースアクセス
- ファイル読み書き
- 一定時間の待機
- 非同期ライブラリの呼び出し
こうした処理では、「結果が返るまで待つ時間」が発生します。
その待ち時間のあいだ、スレッドを無駄にふさがないようにするために、コルーチンとsuspend関数が活躍します。
Thread.sleep()とdelay()の違い
suspend関数を理解するうえで、最重要の比較がこれです。
Thread.sleep()
Thread.sleep(1000)
これはスレッドそのものを1秒間停止します。
そのあいだ、そのスレッドは他の仕事をできません。
delay()
delay(1000)
これはコルーチンを1秒間中断します。
スレッドをブロックしません。
つまり、delay()は「待機中にスレッドを占有しない」点が重要です。
suspendはスレッドを止めるのではなく、コルーチンを中断する
suspendの本質はここにあります。
suspend関数が中断されるとき、止まるのはコルーチンの実行であって、必ずしもスレッドそのものではありません。
このため、待機中のスレッド資源を効率よく使えます。
ただし、ここも厳密に言うと、
- 中断前と再開後で同じスレッドとは限らない
- 「空いたそのスレッドが必ず別の仕事をする」とまでは言い切れない
- 重要なのは「スレッドをブロックしない」こと
です。
つまり理解の中心は、suspendはスレッド資源を無駄に占有しにくくする仕組みという点にあります。
suspend関数はどこから呼べるのか
suspend関数は、基本的にサスペンド可能な文脈から呼び出します。
たとえば次のような場所です。
- 他の
suspend関数の中 launch { ... }の中async { ... }の中runBlocking { ... }の中
たとえばこれはそのままでは呼べません。
suspend fun fetchData(): String {
return "data"
}
fun main() {
val result = fetchData() // エラー
println(result)
}
これはmainが通常の関数であり、サスペンド可能な文脈ではないためです。
runBlockingの役割
通常のコードからsuspend関数を呼ぶには、たとえばrunBlockingを使います。
import kotlinx.coroutines.*
suspend fun fetchData(): String {
delay(1000)
return "data"
}
fun main() = runBlocking {
val result = fetchData()
println(result)
}
runBlockingは、現在のスレッドをブロックしながらコルーチンを実行し、その完了を待つための仕組みです。
そのため、
- サンプルコード
main関数- テストコード
- 一部のブリッジ処理
では便利ですが、UIスレッドなどで安易に多用するものではありません。
suspend関数は自動で並列実行されるわけではない
ここは非常に重要です。
suspend関数は一時停止可能ですが、それだけで並列実行を意味するわけではありません。
たとえば次のコードです。
import kotlinx.coroutines.*
suspend fun task1(): String {
delay(1000)
return "A"
}
suspend fun task2(): String {
delay(1000)
return "B"
}
fun main() = runBlocking {
val a = task1()
val b = task2()
println(a + b)
}
これは順番に実行されます。
task1()を実行- 終わってから
task2()を実行
つまり、suspendは「順次実行のままでも使える」機能です。
デフォルトではコードは普通に上から下へ進みます。
並行実行したいならasyncやlaunchを使う
並行に進めたい場合は、suspendだけではなく、コルーチンを開始する仕組みが必要です。
たとえばasyncを使う例です。
import kotlinx.coroutines.*
suspend fun task1(): String {
delay(1000)
return "A"
}
suspend fun task2(): String {
delay(1000)
return "B"
}
fun main() = runBlocking {
val deferred1 = async { task1() }
val deferred2 = async { task2() }
val result = deferred1.await() + deferred2.await()
println(result)
}
この場合、2つの処理を並行に進められる可能性があります。
ただし、「必ずちょうど半分の時間になる」と断定はできません。
実際の実行時間は、処理内容、dispatcher、実行環境に依存します。
ただ、少なくともdelay()のような待機中心の例では、全体時間を短縮しやすいです。
suspend関数の中では何ができるのか
suspend関数の中では、他のsuspend関数を自然に呼び出せます。
suspend fun login(): String {
delay(500)
return "token"
}
suspend fun fetchUser(token: String): String {
delay(500)
return "user data: $token"
}
suspend fun loadProfile(): String {
val token = login()
return fetchUser(token)
}
このように書けるため、非同期処理をコールバックなしで上から下へ読める形にまとめやすくなります。
これがsuspend関数の大きなメリットです。
suspend関数の中でブロッキング処理を書いたらどうなるか
ここも非常に大事です。
suspend関数の中であっても、ブロッキング処理を書けば、その処理は普通にスレッドを止めます。
suspend fun badExample() {
Thread.sleep(1000)
}
このコードは、suspendが付いていても非ブロッキングにはなりません。
つまり、
delay()はコルーチンを中断するThread.sleep()はスレッドをブロックする
という違いがあります。
suspendを付けただけで、既存のブロッキングAPIが安全な非同期APIに変わるわけではありません。
withContextとの関係
suspend関数を学ぶと、よく一緒に出てくるのがwithContextです。
suspend fun readFile(): String = withContext(Dispatchers.IO) {
"file content"
}
withContextは、実行コンテキストを切り替えるために使います。
たとえば、
Dispatchers.IO→ I/O向きDispatchers.Default→ CPU計算向き
という使い分けをします。
ここでの役割の違いはこうです。
suspend→ この関数はサスペンド可能withContext→ どのコンテキストで実行するかを切り替える
似て見えますが、役割は別です。
launch、async、suspendの違い
この3つは混同しやすいですが、役割を分けて考えると整理しやすいです。
suspend fun
一時停止可能な関数を定義する
suspend fun getData(): String
launch
戻り値なしのコルーチンを開始する
launch {
doSomething()
}
戻り値はJobです。
async
結果を返すコルーチンを開始する
val deferred = async {
getData()
}
val result = deferred.await()
戻り値はDeferred<T>です。
つまり、
suspend= 関数の性質launch/async= コルーチンの開始方法
と考えると分かりやすいです。
suspend関数に戻り値はあるのか
あります。
普通の関数と同じです。
suspend fun sum(a: Int, b: Int): Int {
delay(100)
return a + b
}
もちろんUnitでも書けます。
suspend fun logMessage() {
delay(100)
println("done")
}
suspend関数はインターフェースにも書ける
たとえばこうです。
interface UserRepository {
suspend fun getUser(id: String): String
}
実務では、RepositoryやUseCaseなどの抽象化でよく使います。
また、関数型にも使えます。
val block: suspend () -> String = {
delay(1000)
"hello"
}
例外処理はどうなるのか
suspend関数でも、例外は普通の関数に近い感覚で扱えます。
suspend fun fetchData(): String {
delay(1000)
throw RuntimeException("通信エラー")
}
呼び出し側でtry-catchできます。
fun main() = runBlocking {
try {
val result = fetchData()
println(result)
} catch (e: Exception) {
println("error: ${e.message}")
}
}
コールバックベースの非同期処理より、制御フローを自然に書きやすいのが利点です。
キャンセルとの関係
コルーチンにはキャンセルの仕組みがあります。
suspend関数はこの仕組みと相性が良いです。
たとえばdelay()はキャンセル可能です。
val job = launch {
delay(5000)
println("done")
}
job.cancel()
ただし、ブロッキング処理ばかりしていると、キャンセルに素直に反応しにくくなります。
そのため、キャンセル可能なsuspend関数や適切なAPIを使うことが重要です。
suspendの裏側で起きていること
内部的には、Kotlinコンパイラがsuspend関数を、Continuationを使って再開可能な形へ変換します。
概念的には、次のようなイメージです。
suspend fun fetchData(): String
これは内部で、おおまかには
fun fetchData(continuation: Continuation<String>): Any
のような形に近い構造へ変換されます。
そして、
- どこまで進んだか
- どこから再開するか
- 結果をどう返すか
を管理することで、中断と再開を実現しています。
実務で毎回これを意識する必要はありませんが、suspendの仕組みを深く理解する助けになります。
suspend関数の本当のメリット
suspend関数の価値は、単に「止まれる」ことではありません。
実際の大きな利点は次の通りです。
可読性が高い
非同期処理を、同期処理に近い自然な順序で書けます。
コールバック地獄を避けやすい
処理のネストが深くなりにくいです。
スレッド資源を効率よく使いやすい
待ち時間のある処理に向いています。
例外処理が書きやすい
try-catchで扱いやすくなります。
キャンセルや構造化並行性と組み合わせやすい
大きなアプリでも管理しやすくなります。
初学者が誤解しやすいポイント
suspendを付ければ自動で非同期になる
正確には違います。
suspendは並列実行や自動非ブロッキング化そのものを意味しません。
suspendを付ければブロッキング処理も安全になる
違います。
Thread.sleep()のようなブロッキングAPIは、そのままスレッドを止めます。
suspend関数はどこからでも呼べる
呼べません。
サスペンド可能な文脈が必要です。
suspend = 並列実行
違います。
デフォルトでは順次実行です。
並行実行したいならlaunchやasyncを使います。
どの行でも自由に中断される
これも違います。
中断はサスペンドポイントで起こります。
まず押さえるべき最小セット
最初は次の理解で十分です。
suspend
→ 一時停止可能な関数を定義するdelay
→ スレッドをブロックせずに待機するrunBlocking
→ 通常コードからコルーチンを呼ぶ入口として使うことがあるlaunch
→ 戻り値なしのコルーチンを開始するasync/await
→ 結果付きのコルーチンを開始して取得するwithContext
→ 実行コンテキストを切り替える
典型的なサンプル
import kotlinx.coroutines.*
suspend fun getToken(): String {
delay(500)
return "token123"
}
suspend fun getUserInfo(token: String): String {
delay(500)
return "User(token=$token)"
}
fun main() = runBlocking {
try {
val token = getToken()
val user = getUserInfo(token)
println(user)
} catch (e: Exception) {
println("Error: ${e.message}")
}
}
このコードは、非同期的な待ちを含む処理を、かなり自然に上から下へ書けています。
一言でまとめると
suspend関数は、「コルーチンの中で使う、サスペンドポイントで中断・再開できる関数」です。
そして、理解のポイントは次の4つです。
suspendはスレッドを止める仕組みではなく、コルーチンの中断を扱うsuspendは並列実行そのものを意味しないsuspendを付けてもブロッキングAPIはブロッキングのまま- 非同期処理を読みやすく書くための中心的な仕組みである
以上、Kotlinのsuspend関数についてでした。
最後までお読みいただき、ありがとうございました。










