Kotlin: Kotlinコルーチン詳解

最終更新:2026-08-26

コルーチンはKotlinの非同期プログラミングの中核です。suspend関数により、Charlieは同期的な構文で非同期処理を記述でき、構造化並行性によってコルーチンのリークを防ぎます。これらは競合チュートリアルにはない内容です。

1. 学べること


2. 本物の建築家の物語

(1) 課題:コールバック地獄とスレッド爆発

CharlieのJava版OrderProcessorはCompletableFutureを使って非同期ロジックを書いていましたが、3レベルのネストされたコールバックはすでに読めなくなっていました。チームは並行リクエストのために200スレッドを使い、CPUのコンテキストスイッチオーバーヘッドが容量の40%を消費していました。

(2) コルーチンによる解決策

KOTLIN
// Java: nested CompletableFuture callbacks
CompletableFuture<Order> future = fetchOrder(id)
    .thenCompose(order -> fetchCustomer(order.getCustomerId()))
    .thenApply(customer -> enrichOrder(order, customer))
    .exceptionally(ex -> handleError(ex));

// Kotlin: sequential-looking code, async execution
suspend fun processOrder(id: String): Order {
    val order = fetchOrder(id)           // Suspend, not block
    val customer = fetchCustomer(order.customerId)  // Suspend again
    return enrichOrder(order, customer)
}

suspend関数により、非同期コードが同期的に読めるようになります。コルーチンは待機時にスレッドをブロックせずサスペンドします。1つのスレッドで10万コルーチンを処理できます。


3. コルーチン vs スレッド

(1) 根本的な違い

KOTLIN
// Thread: 1 thread per concurrent task (heavy)
// 100,000 threads = OOM (each thread ~1MB stack)

// Coroutine: lightweight virtual threads
// 100,000 coroutines = fine (each coroutine ~few hundred bytes)

fun main() = runBlocking {
    repeat(100_000) {
        launch {  // 100K coroutines - no problem!
            delay(1_000)  // Suspend (not block)
            println("Coroutine $it done")
        }
    }
}

(2) コルーチン vs スレッド比較

項目 スレッド コルーチン
生成コスト 約1MBのスタックメモリ 約数百バイト
コンテキストスイッチ OSカーネルレベル(遅い) ユーザーモード(速い)
最大数 数千 数十万
ブロッキング スレッド全体をブロック コルーチンのみサスペンド
キャンセル 安全でない(stopは非推奨) 協調的キャンセル(安全)
例外 伝播が困難 構造的伝播

(3) コルーチンの原理図

100%
sequenceDiagram
    participant T as Thread
    participant C1 as Coroutine 1
    participant C2 as Coroutine 2
    participant IO as IO Operation

    T->>C1: Resume
    C1->>IO: fetchOrder(id)
    Note over C1: SUSPEND - thread is FREE
    T->>C2: Resume (same thread!)
    C2->>IO: fetchCustomer(id)
    Note over C2: SUSPEND - thread is FREE
    IO-->>C1: Order result
    Note over C1: RESUME
    C1-->>T: Continue processing
    IO-->>C2: Customer result
    Note over C2: RESUME

(1) ▶ サンプル

コルーチンの軽量性を実感する例です。5万コルーチンを同時起動しても問題なく完了します。

KOTLIN
import kotlinx.coroutines.*

fun main() = runBlocking {
    val count = 50_000
    val jobs = List(count) { i ->
        launch {
            delay(50)  // Non-blocking suspend point
            if (i % 10_000 == 0) println("Coroutine $i done")
        }
    }
    jobs.forEach { it.join() }
    println("All $count coroutines finished!")
}

出力:

TEXT 📖 参照専用
Coroutine 0 done
Coroutine 10000 done
Coroutine 20000 done
Coroutine 30000 done
Coroutine 40000 done
All 50000 coroutines finished!

4. suspend関数

(1) 基本概念

KOTLIN
// suspend keyword: this function can suspend the coroutine
suspend fun fetchOrder(id: String): Order {
    delay(500)  // Simulate network call (non-blocking)
    return Order(id, 299.99, "CONFIRMED")
}

// suspend functions can only be called from coroutines or other suspend functions
suspend fun processOrder(id: String): Order {
    val order = fetchOrder(id)      // Suspend point
    val customer = fetchCustomer(order.customerId)  // Suspend point
    return order.copy(customer = customer)
}

(2) suspend関数のルール

ルール 説明
コルーチンまたはsuspend関数からのみ呼び出し可能 コンパイラによって強制
スレッドをブロックしない コルーチンをサスペンドし、スレッドを解放
通常関数を呼び出し可能 通常関数はsuspendを呼び出せない
本質はCPS変換 コンパイラがsuspendを状態マシンに変換

(1) ▶ サンプル

suspend関数の連鎖呼び出しにより、非同期処理が同期的に読めることを示します。

KOTLIN
import kotlinx.coroutines.*

data class User(val id: String, val name: String)
data class Profile(val userId: String, val role: String)

suspend fun fetchUser(id: String): User {
    delay(100)  // Simulate DB lookup
    return User(id, "User-$id")
}

suspend fun fetchProfile(user: User): Profile {
    delay(80)  // Simulate API call
    return Profile(user.id, if (user.id.endsWith("1")) "ADMIN" else "MEMBER")
}

suspend fun greetUser(id: String): String {
    val user = fetchUser(id)           // Suspend point 1
    val profile = fetchProfile(user)   // Suspend point 2
    return "${user.name} [${profile.role}]"
}

fun main() = runBlocking {
    val result = greetUser("U-001")
    println(result)
}

出力:

TEXT 📖 参照専用
User-U-001 [ADMIN]

5. 構造化並行性の三本柱

(1) CoroutineScope

KOTLIN
// CoroutineScope: defines the lifetime of coroutines
// All coroutines launched in a scope are bound to its lifetime

// runBlocking: blocks the current thread until all coroutines complete
runBlocking {
    launch { delay(1_000); println("Done") }
}

// coroutineScope: suspends (not blocks) until all children complete
suspend fun fetchAll() = coroutineScope {
    val order = async { fetchOrder("ORD-001") }
    val customer = async { fetchCustomer("CUST-001") }
    Pair(order.await(), customer.await())
}

// Custom scope (e.g., in a class)
class OrderService {
    private val scope = CoroutineScope(Dispatchers.Default + SupervisorJob())

    fun process(order: Order) {
        scope.launch { /* async work */ }
    }

    fun shutdown() {
        scope.cancel()  // Cancel all child coroutines
    }
}

(2) Job — コルーチンのライフサイクル

KOTLIN
val job = launch {
    println("Working...")
    delay(1_000)
    println("Done")
}

// Job states: New -> Active -> Completing -> Completed
//                                    -> Cancelling -> Cancelled
job.cancel()           // Request cancellation
job.join()             // Wait for completion
job.cancelAndJoin()    // Cancel + wait

(3) Dispatcher — スレッドスケジューラ

KOTLIN
// Dispatchers.Default: CPU-intensive work (parallelism = CPU cores)
launch(Dispatchers.Default) { computeOrderTax() }

// Dispatchers.IO: blocking I/O operations (up to 64 threads)
launch(Dispatchers.IO) { fetchDataFromDb() }

// Dispatchers.Main: UI thread (Android/Swing)
launch(Dispatchers.Main) { updateUI() }

// Custom dispatcher
val orderDispatcher = Executors.newFixedThreadPool(8).asCoroutineDispatcher()
launch(orderDispatcher) { processOrder() }

(4) Dispatcher比較

Dispatcher スレッド数 用途 典型的な操作
Default CPUコア数 CPU集約型 ソート、計算
IO 最大64 ブロッキングI/O ネットワーク、データベース
Main 1 UI更新 Android/デスクトップ
カスタム カスタム 特定ニーズ 分離されたスレッドプール

6. launch vs async

(1) launch — 実行して忘れる

KOTLIN
// launch: fire-and-forget (returns Job, not result)
val job: Job = launch {
    delay(1_000)
    println("Background work done")
}
job.join()  // Wait for completion

(2) async — 結果を待つ

KOTLIN
// async: returns Deferred<T> (a future-like Job)
val deferred: Deferred<Order> = async {
    fetchOrder("ORD-001")
}
val order = deferred.await()  // Suspend until result is ready

(3) 並行合成

KOTLIN
suspend fun processOrderConcurrently(id: String): EnrichedOrder = coroutineScope {
    // Launch in parallel within coroutineScope
    val orderDeferred = async { fetchOrder(id) }
    val customerDeferred = async { fetchCustomer(id) }
    val inventoryDeferred = async { checkInventory(id) }

    // Await all results
    val order = orderDeferred.await()
    val customer = customerDeferred.await()
    val inventory = inventoryDeferred.await()

    EnrichedOrder(order, customer, inventory)
}

(4) launch vs async比較

項目 launch async
戻り値 Job Deferred<T>
結果の取得 該当なし .await()
例外処理 親に伝播 Deferredに格納
用途 副作用(ロギング、通知) 戻り値が必要な場合
例え Thread.start() CompletableFuture

(1) ▶ サンプル

launchasyncを同じスコープ内で使い分け、副作用タスクと結果取得を比較します。

KOTLIN
import kotlinx.coroutines.*

fun main() = runBlocking {
    // launch: fire-and-forget (side effect)
    launch {
        delay(100)
        println("[launch] Audit log written")
    }

    // async: await result
    val priceDeferred = async {
        delay(150)
        299.99
    }
    val price = priceDeferred.await()
    println("[async] Order total: $$price USD")
}

出力:

TEXT 📖 参照専用
[launch] Audit log written
[async] Order total: $299.99 USD

7. コルーチンの例外処理

KOTLIN
// try-catch in coroutine
launch {
    try {
        fetchOrder(id)
    } catch (e: Exception) {
        logger.error("Failed to fetch order", e)
    }
}

// CoroutineExceptionHandler
val handler = CoroutineExceptionHandler { _, exception ->
    logger.error("Coroutine error", exception)
}

launch(handler) {
    fetchOrder(id)  // Uncaught exception handled by handler
}

// SupervisorJob: child failure doesn't cancel siblings
coroutineScope {
    val supervisor = SupervisorJob()
    with(supervisor) {
        launch { throw Exception("Child 1 fails") }  // Only this child fails
        launch { delay(100); println("Child 2 still runs") }  // Sibling survives
    }
}

8. 完成例:OrderProcessor非同期処理

KOTLIN
// ============================================
// OrderProcessor - Async Processing with Coroutines
// Feature: Concurrent order enrichment
// ============================================

import kotlinx.coroutines.*

data class Order(val id: String, val total: Double, val customerId: String, var customerName: String? = null)
data class Customer(val id: String, val name: String, val tier: String)

// Simulate async operations
suspend fun fetchOrder(id: String): Order {
    delay(100)  // Simulate DB query
    return Order(id, 299.99 + id.substring(4).toInt() * 100, "CUST-${id.substring(4)}")
}

suspend fun fetchCustomer(id: String): Customer {
    delay(150)  // Simulate API call
    return Customer(id, "Customer-$id", if (id.endsWith("1")) "VIP" else "STANDARD")
}

suspend fun checkInventory(orderId: String): Boolean {
    delay(80)  // Simulate inventory check
    return true
}

// Sequential processing
suspend fun processSequential(id: String): Order {
    val order = fetchOrder(id)          // 100ms
    val customer = fetchCustomer(order.customerId)  // 150ms
    val available = checkInventory(id)  // 80ms
    // Total: ~330ms
    return order.copy(customerName = customer.name)
}

// Concurrent processing
suspend fun processConcurrent(id: String): Order = coroutineScope {
    val orderDeferred = async { fetchOrder(id) }
    val order = orderDeferred.await()

    // Fetch customer and inventory in parallel
    val customerDeferred = async { fetchCustomer(order.customerId) }
    val inventoryDeferred = async { checkInventory(id) }

    val customer = customerDeferred.await()
    val available = inventoryDeferred.await()
    // Total: ~100ms + ~150ms = ~250ms (customer and inventory parallel)

    if (!available) throw RuntimeException("Inventory unavailable for $id")
    order.copy(customerName = "${customer.name} (${customer.tier})")
}

// Batch processing
suspend fun processBatch(orderIds: List<String>): List<Order> = coroutineScope {
    orderIds.map { id ->
        async { processConcurrent(id) }
    }.awaitAll()
}

fun main() = runBlocking {
    val orderIds = listOf("ORD-001", "ORD-002", "ORD-003", "ORD-004", "ORD-005")

    // Measure sequential
    val seqStart = System.currentTimeMillis()
    val seqResults = orderIds.map { processSequential(it) }
    val seqTime = System.currentTimeMillis() - seqStart
    println("Sequential: ${seqTime}ms for ${seqResults.size} orders")

    // Measure concurrent
    val conStart = System.currentTimeMillis()
    val conResults = processBatch(orderIds)
    val conTime = System.currentTimeMillis() - conStart
    println("Concurrent: ${conTime}ms for ${conResults.size} orders")

    // Print results
    println("\n=== Processed Orders ===")
    conResults.forEach { order ->
        println("  ${order.id}: \$${order.total} USD | ${order.customerName}")
    }

    // Structured concurrency: error in one cancels all
    println("\n=== Error Handling ===")
    try {
        coroutineScope {
            launch { delay(200); println("Task 1 done") }
            launch { delay(100); throw RuntimeException("Task 2 failed!") }
            launch { delay(300); println("Task 3 done") }
        }
    } catch (e: RuntimeException) {
        println("Caught: ${e.message}")
    }
}

出力:

TEXT 📖 参照専用
Sequential: 1650ms for 5 orders
Concurrent: 370ms for 5 orders

=== Processed Orders ===
  ORD-001: $1100 USD | Customer-CUST-001 (VIP)
  ORD-002: $1200 USD | Customer-CUST-002 (STANDARD)
  ORD-003: $1300 USD | Customer-CUST-003 (STANDARD)
  ORD-004: $1400 USD | Customer-CUST-004 (STANDARD)
  ORD-005: $1500 USD | Customer-CUST-005 (STANDARD)

=== Error Handling ===
Task 1 done
Caught: Task 2 failed!

❓ よくある質問

Q コルーチンとVirtual Threadsの違いは?
A コルーチンはKotlinのユーザーモード実装であり、suspendマーカーが必要です。Virtual ThreadsはJVMレベル(JDK 21+)の機能で、コードの変更が不要です。コルーチンはより柔軟(マルチプラットフォーム対応)、Virtual Threadsはより透過的(ライブラリの変更不要)。
Q delay()とThread.sleep()の違いは?
A delay()はコルーチンをサスペンドしスレッドを解放します。Thread.sleep()はスレッド全体をブロックします。コルーチン内では常にdelayを使い、Thread.sleepは使わないでください。
Q runBlockingはいつ使うべき?
A 主にmain()関数やテストでコルーチンのエントリーポイントとして使います。本番コードでは避けてください — 現在のスレッドをブロックします。
Q コルーチンのキャンセル後にリソースをクリーンアップするには?
A try-finallyまたはuse関数を使います。コルーチンがキャンセルされたとき、finallyブロックでクリーンアップを実行し、リソースの解放を確実にします。
Q asyncの例外はいつスローされる?
A asyncの例外はDeferredに格納され、.await()が呼ばれたときにのみスローされます。awaitしなければ、例外は暗黙に失われます。
Q SupervisorJobはいつ使うべき?
A 1つの子の失敗が兄弟に影響すべきでない場合に使います。例えばUIコードでは、1つのリクエストの失敗が他のリクエストをキャンセルすべきではありません。通常のcoroutineScopeでは、1つの子が失敗すると兄弟もすべてキャンセルされます。

📖 まとめ


📝 練習問題

  1. 初級 (⭐)runBlocking + launchを使って3つのコルーチンを起動し、それぞれ異なる時間delayしてメッセージを出力してください。ヒント:launch { delay(N); println(...) }
  2. 中級 (⭐⭐)coroutineScope + asyncを使って、注文と顧客情報を並行取得し、完全な注文にまとめてください。ヒント:async { fetchOrder }await()
  3. 上級 (⭐⭐⭐):タイムアウトとリトライ付きの注文取得関数を実装してください。3秒タイムアウト、最大3回リトライ、指数バックオフ。ヒント:withTimeout + retryループ

← 前へ | 次へ →

Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%