404 Not Found

404 Not Found


nginx

Kotlin Coroutines Explained

Coroutines are the core of Kotlin's async programming — the suspend function lets Charlie write async operations with synchronous syntax, while structured concurrency ensures no leaked coroutines, content completely absent from competing tutorials.

1. What You'll Learn


2. A Real Architect's Story

(1) Pain Point: Callback Hell and Thread Explosion

Charlie's Java version of OrderProcessor used CompletableFuture for async logic — 3 levels of nested callbacks were already unreadable. The team used 200 threads for concurrent requests, with CPU context-switch overhead consuming 40% of capacity.

(2) The Coroutine Solution

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 functions make async code read like synchronous — coroutines suspend rather than block threads when waiting. One thread can handle 100,000 coroutines.


3. Coroutines vs Threads

(1) Core Differences

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) Coroutine vs Thread Comparison

Dimension Thread Coroutine
Creation cost ~1MB stack memory ~hundreds of bytes
Context switch OS kernel-level (slow) User-mode (fast)
Maximum count Thousands Hundreds of thousands
Blocking Blocks entire thread Only suspends coroutine
Cancellation Unsafe (stop deprecated) Cooperative cancellation (safe)
Exceptions Hard to propagate Structured propagation

(3) Coroutine Principle Diagram

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

4. suspend Functions

(1) Basic Concepts

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 Function Rules

Rule Description
Only callable from coroutines or suspend functions Compiler-enforced
Does not block threads Suspends the coroutine, frees the thread
Can call regular functions Regular functions cannot call suspend
Essentially CPS transformed Compiler converts suspend into a state machine

5. The Three Pillars of Structured Concurrency

(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 — Coroutine Lifecycle

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 — Thread Scheduler

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 Comparison

Dispatcher Thread Count Use Case Typical Operations
Default CPU cores CPU-intensive Sorting, computation
IO Up to 64 Blocking I/O Network, database
Main 1 UI updates Android/Desktop
Custom Custom Specific needs Isolated thread pool

6. launch vs async

(1) launch — Fire-and-Forget

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 — Await Result

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) Concurrent Composition

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 Comparison

Dimension launch async
Return value Job Deferred<T>
Get result N/A .await()
Exception handling Propagates to parent Stored in Deferred
Use case Side effects (logging, notifications) Need return value
Analogy Thread.start() CompletableFuture

7. Coroutine Exception Handling

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. Complete Example: OrderProcessor Async Processing

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

Output:

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!

❓ FAQ

Q What's the difference between coroutines and Virtual Threads?
A Coroutines are Kotlin's user-mode implementation requiring suspend markers; Virtual Threads are JVM-level (JDK 21+) requiring no code changes. Coroutines are more flexible (multiplatform), while Virtual Threads are more transparent (no library changes).
Q What's the difference between delay() and Thread.sleep()?
A delay() suspends the coroutine and frees the thread; Thread.sleep() blocks the entire thread. In coroutines, always use delay, never Thread.sleep.
Q When should I use runBlocking?
A Primarily in main() functions and tests as the coroutine entry point. Avoid in production code — it blocks the current thread.
Q How do I clean up resources after coroutine cancellation?
A Use try-finally or use functions. Execute cleanup in the finally block when a coroutine is cancelled to ensure resource release.
Q When is an async exception thrown?
A async's exception is stored in the Deferred and thrown only when .await() is called. If you never await, the exception is silently lost.
Q When should I use SupervisorJob?
A When one child's failure shouldn't affect siblings. For example, in UI code, one request failing shouldn't cancel others. In a normal coroutineScope, one child failing cancels all siblings.

📖 Summary


📝 Exercises

  1. Beginner (⭐): Use runBlocking + launch to start 3 coroutines, each delaying a different time and printing a message. Hint: launch { delay(N); println(...) }
  2. Intermediate (⭐⭐): Use coroutineScope + async to concurrently fetch order and customer info, combining them into a complete order. Hint: async { fetchOrder }, await()
  3. Advanced (⭐⭐⭐): Implement an order fetch function with timeout and retry: 3-second timeout, up to 3 retries, exponential backoff. Hint: withTimeout + retry loop

← Previous | Next →

Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏