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
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
} 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")
} 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?
2. What does a suspend function do when it reaches a suspension point?
3. What is the difference between launch and async?
4. What is structured concurrency?
Flashcards
Question
What is a suspend function?
Click to reveal answer
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?
Click to reveal answer
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?
Click to reveal answer
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?
Click to reveal answer
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 Jobasync { }- 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 blockingawait()- wait for async resultcancel()- stop a coroutine