Kotlin: Kotlinコルーチン詳解
最終更新:2026-08-26
コルーチンはKotlinの非同期プログラミングの中核です。suspend関数により、Charlieは同期的な構文で非同期処理を記述でき、構造化並行性によってコルーチンのリークを防ぎます。これらは競合チュートリアルにはない内容です。
1. 学べること
- コルーチン vs スレッド:軽量、非ブロッキング、構造化並行性
suspend関数:非同期の世界における「同期的な構文」CoroutineScope/Job/Dispatcher:構造化並行性の三本柱launch(実行して忘れる)vsasync(結果を待つ)- Charlieの実践:
async+awaitによる並行OrderProcessor
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) コルーチンの原理図
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) ▶ サンプル
launchとasyncを同じスコープ内で使い分け、副作用タスクと結果取得を比較します。
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つの子が失敗すると兄弟もすべてキャンセルされます。📖 まとめ
- コルーチンは軽量スレッドであり、生成コストが極めて低い(数百バイト vs 1MB)
suspend関数により非同期コードが同期的に読める — サスペンドし、ブロックしない- 構造化並行性:
coroutineScopeはすべての子コルーチンの完了またはキャンセルを保証 launchは実行して忘れる;asyncは戻り値を待つDispatcherはどのスレッドプールで実行するかを制御:Default / IO / Main- 例外処理:
try-catch/CoroutineExceptionHandler/SupervisorJob
📝 練習問題
- 初級 (⭐):
runBlocking+launchを使って3つのコルーチンを起動し、それぞれ異なる時間delayしてメッセージを出力してください。ヒント:launch { delay(N); println(...) } - 中級 (⭐⭐):
coroutineScope+asyncを使って、注文と顧客情報を並行取得し、完全な注文にまとめてください。ヒント:async { fetchOrder }、await() - 上級 (⭐⭐⭐):タイムアウトとリトライ付きの注文取得関数を実装してください。3秒タイムアウト、最大3回リトライ、指数バックオフ。ヒント:
withTimeout+retryループ