0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

[Kotlin Coroutine] Coroutine の基本と Non-Blocking 設計の考え方

Last updated at Posted at 2025-12-23

はじめに

上記の記事でも書きましたが、今年実施したプロジェクトで Spring Boot(Kotlin)+ gRPC を利用した構成を採用し、アプリケーション全体として可能な限り Non-Blocking な設計を取りました。

チーム内での勉強会の資料も兼ねて、Kotlin で Non-Blocking な処理を書く際に前提となる Coroutine(コルーチン)について整理します。

Coroutine の概要から、Blocking / Non-Blocking の違い、実務で注意すべきポイントまでをまとめます。

Coroutine とは?

Coroutine は Kotlin における軽量な非同期処理の仕組みです。

スレッドを直接操作することなく、

  • 非同期処理を
  • 同期処理のような記述で
  • 構造的に制御できる

という点が大きな特徴です。

Coroutine はスレッドそのものを置き換えるものではなく、
スレッド上で動作する処理の流れを効率よく制御するための抽象化
と考えると理解しやすいです。

Blocking と Non-Blocking の違い

Coroutine を理解する上で、Blocking と Non-Blocking の違いは非常に重要です。

観点 Blocking Non-Blocking
スレッドの扱い 処理中はスレッドを占有 I/O 待ち時はスレッドを解放
同時リクエスト数 スレッド数に依存しやすい(例:200スレッド ⇒ 最大200リクエスト) スレッド数に依存しにくい(例:200スレッド ⇒ 数千リクエスト)
CPU負荷 コンテキストスイッチが増えがち 少ないスレッドで回せる
メモリ消費 スレッドスタックが重い Coroutine は軽量
実装難易度 簡単 難しい(設計が重要)

但し、Coroutine 自体が自動的に Non-Blocking になるわけではなく、
suspend 関数の中身や Dispatcher の使い方によってはスレッドをブロックしてしまうため注意が必要です。

Coroutine を使う利点

処理が複雑になりがちな Non-Blocking 処理を、比較的直感的に記述できます。

コードが読みやすい

val user = fetchUser()          // User(id=1, name="Alice")
val items = fetchItems(user.id) // [Item(id=10), Item(id=11)]

非同期処理であっても、処理の流れを上から下へ自然に読むことができます。

軽量でスケーラブル

Coroutine はスレッドよりも非常に軽量であり、多数同時に起動できます。
特に I/O 待ちが多い処理では、スレッドを占有しない点が効果的です。

例外処理・キャンセル制御が一貫している

CoroutineScope を単位として、例外やキャンセルを構造的に管理できます。
これにより、処理の境界が明確になります。

CompletableFuture との比較

観点 CompletableFuture Coroutine
記述 thenCompose 等で複雑になりがち 同期処理のように記述可能
例外処理 独自 API が必要 try-catch で自然
戻り値 Future 経由 通常の変数
Kotlin との親和性 低め 非常に高い
CompletableFuture
    .supplyAsync(() -> fetchUser())
    .thenCompose(user -> fetchItems(user.getId()));
val user = fetchUser()
val items = fetchItems(user.id)

Java では CompletableFuture が有力な選択肢ですが、
Kotlin を利用する場合は Coroutine の方が可読性・保守性ともに高くなるケースが多いと感じます。

suspend 関数について

suspend の意味

suspend fun fetchUser(): User

suspend は、その関数が途中で一時停止・再開される可能性があることを示します。
I/O 待ちなどでスレッドをブロックせずに待機できる点が特徴です。

なお、suspend 関数は CoroutineScope 内、または他の suspend 関数からのみ呼び出せます。

アンチパターン(suspend を付けてもスレッドは止まる)

suspend fun bad(id: Int) {
    println("start bad: $id")
    Thread.sleep(1000) // Blocking
    println("end bad: $id")
}
runBlocking {
    repeat(4) {
        launch {
            bad(it)
        }
    }
}

出力例(Blocking)

start bad: 0
start bad: 1
(1秒間何も出力されない)
end bad: 0
end bad: 1
start bad: 2
start bad: 3
(さらに1秒)
end bad: 2
end bad: 3

suspend を付けていても、内部で Blocking API を呼び出すと
Coroutine が実行されている スレッドそのものが占有 されます。

Blocking な場合のイメージ

時間 →
Thread-1: ■■■■■■■■■■  (Thread.sleep)
Thread-2: ■■■■■■■■■■  (Thread.sleep)
Thread-3: ○○○○○○○○○  (スレッドはあるが処理能力を活かせない)
Thread-4: ○○○○○○○○○  (スレッドはあるが処理能力を活かせない)

■ : Blocking 処理で占有されている(使えない)
○ : 存在はするが有効活用できていない

Non-Blocking な場合

suspend fun good(id: Int) {
    println("start good: $id")
    delay(1000) // Non-Blocking
    println("end good: $id")
}
runBlocking {
    repeat(4) {
        launch {
            good(it)
        }
    }
}

出力例(Non-Blocking)

start good: 0
start good: 1
start good: 2
start good: 3
(1秒後)
end good: 0
end good: 1
end good: 2
end good: 3

delay は待機中にスレッドを占有しないため、
同じスレッドを使って他の Coroutine が次々と実行されます。

ベストプラクティス

  • 時間がかかる処理や I/O は suspend 関数として定義する
  • Blocking API を使う場合は Dispatcher を切り替えて隔離する
  • 利用ライブラリ内でスレッドをブロックする処理がないかを意識する

Coroutine の基本的な使い方

起動方法

scope.launch {
    doSomething()
}
val result = scope.async {
    heavyTask()
}.await()
  • launch:戻り値が不要な処理
  • async:結果を返す処理

CoroutineScope

CoroutineScope(Dispatchers.IO)

Scope がキャンセルされると、その配下の Coroutine もすべてキャンセルされます。
GlobalScope の使用は基本的に推奨されません。

Dispatchers

Dispatcher 主な用途
Dispatchers.IO Blocking な DB / ファイル I/O
Dispatchers.Default CPU負荷の高い処理
Dispatchers.Main UI スレッド(Android / Desktop)
withContext(Dispatchers.IO) {
    blockingRepository.findAll()
}

Dispatchers.IOBlocking API を扱うための Dispatcher です。

但し、R2DBC のような Non-Blocking API を利用する場合は Dispatchers.IO への切り替えは基本的に不要で、むしろ非推奨です

async / await による並列処理

suspend fun taskA(): Int {
    delay(500)
    println("taskA done")
    return 1
}

suspend fun taskB(): Int {
    delay(500)
    println("taskB done")
    return 2
}
val result = coroutineScope {
    val a = async { taskA() }
    val b = async { taskB() }
    a.await() + b.await()
}

println("result = $result")

出力例

taskA done
taskB done
result = 3

タイムアウトとキャンセル

withTimeout(1000) {
    longTask()
}
job.cancel()

キャンセルは協調的に行われるため、
suspend 関数側でキャンセルを考慮した実装が重要です。

supervisorScope

supervisorScope {
    launch { taskA() }
    launch { taskB() }
}

実装例(Spring Boot)

@RestController
class SampleController {

    @GetMapping("/hello")
    suspend fun hello(): String {
        delay(100)
        return "Hello Coroutine"
    }
}

レスポンス例

GET /hello

HTTP/1.1 200 OK
Hello Coroutine

delay は Non-Blocking で待機します。
Spring WebFlux と組み合わせることで、I/O レベルから Non-Blocking な Controller を実装できます。

なお、Spring MVC でも Coroutine は利用できますが、
I/O モデル自体は Blocking である点には注意が必要です。

終わりに

Coroutine は Kotlin における非同期処理をシンプルに書くための強力な仕組みですが、
それだけで自動的に Non-Blocking になるわけではありません。

suspend 関数の中身や Dispatcher の使い分け、さらにフレームワークの I/O モデルまで含めて設計することで、初めて Non-Blocking 構成のメリットを活かすことができます。

本記事が、Coroutine を使った実装を考える際の整理や、Non-Blocking を意識した設計を見直すきっかけになれば幸いです。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?