Kotlinのsuspend関数について

採用はこちら

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)
}

これは順番に実行されます。

  1. task1()を実行
  2. 終わってからtask2()を実行

つまり、suspendは「順次実行のままでも使える」機能です。

デフォルトではコードは普通に上から下へ進みます。

並行実行したいならasynclaunchを使う

並行に進めたい場合は、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 → どのコンテキストで実行するかを切り替える

似て見えますが、役割は別です。

launchasyncsuspendの違い

この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 = 並列実行

違います。

デフォルトでは順次実行です。

並行実行したいならlaunchasyncを使います。

どの行でも自由に中断される

これも違います。

中断はサスペンドポイントで起こります。

まず押さえるべき最小セット

最初は次の理解で十分です。

  • 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関数についてでした。

最後までお読みいただき、ありがとうございました。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次