Skip to content
intermediate Phase 1 · Kotlin Foundations

Coroutines Introduction

Understand Kotlin coroutines: suspend functions, launching coroutines, and structured concurrency basics.

50m
2 problems
Topic Progress 0%

Why Coroutines?

The Problem with Threads

Traditional multithreading uses one thread per concurrent task. Threads are expensive: each thread consumes ~1MB of stack memory, and switching between threads has significant CPU overhead. On Android, you cannot block the main thread (it handles UI), so async work requires callbacks, which leads to callback hell.

// Java callback hell
fetchUser(userId, new Callback<User>() {
    @Override
    public void onSuccess(User user) {
        fetchOrders(user.id, new Callback<List<Order>>() {
            @Override
            public void onSuccess(List<Order> orders) {
                // Nested callbacks become unreadable
            }
        });
    }
});

Coroutines as Lightweight Alternatives

Coroutines are functions that can suspend execution and resume later. They run on threads but are not tied to them. A single thread can run thousands of coroutines because coroutines use ~few hundred bytes of memory each and switching between them is cheap.

// Kotlin coroutines - flat, readable
suspend fun fetchUserData(userId: String) {
    val user = fetchUser(userId)        // Suspends, not blocks
    val orders = fetchOrders(user.id)   // Resumes when ready
    displayOrders(orders)
}

The code reads like sequential blocking code, but it does not block the thread. When a coroutine reaches a suspension point (a suspend function call), it releases the thread so other coroutines can run. When the suspended work completes, the coroutine resumes from where it left off.

How Suspension Works

When you call a suspend function, the compiler transforms it into a state machine. Each suspension point is a state. The coroutine saves its local variables and resumes from the saved state when the suspended operation completes.

// Pseudocode of what the compiler generates
fun fetchUserData(state: Continuation<User>) {
    when (state.label) {
        0 -> {
            state.label = 1
            val user = fetchUser(userId, state)  // Suspends here
            return
        }
        1 -> {
            val user = state.result as User
            state.label = 2
            val orders = fetchOrders(user.id, state)  // Suspends here
            return
        }
        2 -> {
            val orders = state.result as List<Order>
            displayOrders(orders)
        }
    }
}

This transformation is invisible to the programmer. You write sequential code; the compiler handles the state management.

Launching Coroutines

Coroutine Builders

Kotlin provides functions to create coroutines. The most common are launch and async:

import kotlinx.coroutines.*

fun main() = runBlocking {
    // launch: fire-and-forget
    launch {
        delay(1000)
        println("World")
    }
    println("Hello")
}
// Output:
// Hello
// (1 second later)
// World

launch starts a new coroutine and returns a Job. The coroutine runs concurrently with the calling code.

delay vs Thread.sleep

delay is a suspend function that pauses the coroutine without blocking the thread. Thread.sleep blocks the thread entirely:

launch {
    delay(1000)  // Coroutine suspends; thread is free
    println("Done")
}

// Thread.sleep(1000)  // Blocks the thread entirely; avoid in coroutines

async and await

async starts a coroutine that returns a result. Use await() to retrieve it:

fun main() = runBlocking {
    val deferred1 = async { fetchUser(1) }
    val deferred2 = async { fetchUser(2) }

    // Both run concurrently
    val user1 = deferred1.await()
    val user2 = deferred2.await()
    println("Fetched: $user1, $user2")
}

Without async/await, sequential calls would take the sum of both durations. With concurrent execution, they take the max.

runBlocking

runBlocking bridges blocking and non-blocking worlds. It blocks the current thread until all its coroutines complete. Use it only in main() functions and tests, never in library code:

fun main() = runBlocking {
    launch { delay(1000); println("Done") }
}
// main() blocks here until the coroutine completes

Structured Concurrency

Every coroutine must run inside a scope. The scope manages the coroutine lifecycle. When the scope is canceled, all its children are canceled:

fun main() = runBlocking {
    launch {
        delay(1000)
        println("Child 1")
    }
    launch {
        delay(500)
        println("Child 2")
    }
    println("Parent")
}
// Parent
// (500ms) Child 2
// (1000ms) Child 1

If a child coroutine fails, the parent is notified. If the parent is canceled, all children are canceled. This prevents resource leaks.

Exception Handling

Use CoroutineExceptionHandler to handle uncaught exceptions in coroutines:

val handler = CoroutineExceptionHandler { _, exception ->
    println("Caught: $exception")
}

fun main() = runBlocking(handler) {
    launch {
        throw RuntimeException("Boom")
    }
}
// Caught: java.lang.RuntimeException: Boom

Practice Problems

0 / 3 solved
Concurrent Fetcher

Write a function that fetches two values concurrently using async and returns them as a pair. Use delay to simulate network calls (500ms each). The total time should be ~500ms, not ~1000ms.

Solution
import kotlinx.coroutines.*
import kotlin.system.measureTimeMillis

suspend fun fetchValue(id: Int): String {
    delay(500)
    return "Value$id"
}

fun main() = runBlocking {
    val time = measureTimeMillis {
        val result1 = async { fetchValue(1) }
        val result2 = async { fetchValue(2) }
        val pair = Pair(result1.await(), result2.await())
        println(pair)
    }
    println("Took ${time}ms")  // ~500ms
}
Sequential vs Concurrent

Write two versions of a function that computes the sum of two lists. Version 1: sequential (compute sum of list1, then sum of list2). Version 2: concurrent (compute both sums in parallel with async). Measure and compare the times.

Solution
import kotlinx.coroutines.*
import kotlin.system.measureTimeMillis

suspend fun slowSum(list: List<Int>): Int {
    delay(1000)
    return list.sum()
}

fun main() = runBlocking {
    val list1 = listOf(1, 2, 3)
    val list2 = listOf(4, 5, 6)

    val seqTime = measureTimeMillis {
        val sum1 = slowSum(list1)
        val sum2 = slowSum(list2)
        println("Sequential: ${sum1 + sum2}")
    }

    val concTime = measureTimeMillis {
        val sum1 = async { slowSum(list1) }
        val sum2 = async { slowSum(list2) }
        println("Concurrent: ${sum1.await() + sum2.await()}")
    }

    println("Sequential: ${seqTime}ms, Concurrent: ${concTime}ms")
}
Structured Concurrency Demo

Create a coroutine scope that launches 3 child coroutines with different delays (300ms, 100ms, 200ms). Cancel the scope after 150ms. Verify that not all children complete.

Solution
import kotlinx.coroutines.*

fun main() = runBlocking {
    val scope = CoroutineScope(Job())

    scope.launch {
        delay(300)
        println("Child 1 completed")
    }
    scope.launch {
        delay(100)
        println("Child 2 completed")
    }
    scope.launch {
        delay(200)
        println("Child 3 completed")
    }

    delay(150)
    scope.cancel()
    println("Scope canceled")
    delay(500)
}
// Output:
// Child 2 completed
// Scope canceled

Quiz

1. What is the main advantage of coroutines over threads?

Question 1 options

2. What does a suspend function do when it reaches a suspension point?

Question 2 options

3. What is the difference between launch and async?

Question 3 options

4. What is structured concurrency?

Question 4 options

Flashcards

Question

What is a suspend function?

Answer

A function that can pause execution at suspension points without blocking the thread. The thread is freed to run other coroutines. The function resumes when the suspended operation completes.

Question

What does launch return?

Answer

A Job object. The coroutine runs concurrently. Use the Job to cancel the coroutine. launch is for fire-and-forget operations that do not produce a result.

Question

What does async/await do?

Answer

async starts a coroutine that computes a result. await() suspends until the result is available. Use async when you need to retrieve a value from a coroutine.

Question

Why avoid Thread.sleep in coroutines?

Answer

Thread.sleep blocks the entire thread, preventing other coroutines from running. Use delay() instead, which suspends the coroutine without blocking the thread.

Revision Notes

Key Takeaways

  • 1. Coroutines are lightweight and use far less memory than threads.
  • 2. suspend functions release the thread at suspension points.
  • 3. launch is for side effects; async is for computations with results.
  • 4. Structured concurrency prevents resource leaks through lifecycle management.
  • 5. delay() suspends the coroutine; Thread.sleep() blocks the thread.

Interview Tips

  • Explain the difference between blocking and suspending.
  • Know when to use launch vs async with concrete scenarios.
  • Understand structured concurrency and why it prevents resource leaks.
  • Be ready to compare coroutines with callbacks and reactive streams.

Cheat Sheet

Coroutines Introduction Cheat Sheet

Key Concepts:

  • Coroutine = lightweight unit of work that can suspend
  • Suspend function = function that can pause at suspension points
  • Thread runs many coroutines sequentially

Builders:

  • launch { } - fire-and-forget, returns Job
  • async { } - returns Deferred (result via await())
  • runBlocking { } - bridges blocking and coroutine worlds

Structured Concurrency:

  • Coroutines run inside a scope
  • Parent scope manages children lifecycle
  • Canceling scope cancels all children
  • Child failure propagates to parent

Key Functions:

  • delay(ms) - suspend without blocking
  • await() - wait for async result
  • cancel() - stop a coroutine