Coroutines Internals & CPS Transformation
📖 Concept
Kotlin Coroutines are the foundation of async programming in modern Android. At the senior level, you need to understand how they work under the hood — not just how to use them.
How coroutines work internally:
The Kotlin compiler transforms suspend functions using Continuation-Passing Style (CPS). Each suspend function receives a hidden Continuation parameter and is compiled into a state machine.
CPS Transformation:
// What you write:
suspend fun fetchUser(): User {
val token = getToken() // suspension point 1
val user = getUser(token) // suspension point 2
return user
}
// What the compiler generates (simplified):
fun fetchUser(continuation: Continuation<User>): Any? {
val sm = continuation as? FetchUserSM ?: FetchUserSM(continuation)
when (sm.label) {
0 -> {
sm.label = 1
val result = getToken(sm) // may return COROUTINE_SUSPENDED
if (result == COROUTINE_SUSPENDED) return COROUTINE_SUSPENDED
sm.tokenResult = result
}
1 -> {
sm.label = 2
val result = getUser(sm.tokenResult, sm)
if (result == COROUTINE_SUSPENDED) return COROUTINE_SUSPENDED
sm.userResult = result
}
2 -> return sm.userResult
}
}
Key concepts:
- Continuation: A callback that knows how to resume the coroutine. Contains the state machine and label.
- COROUTINE_SUSPENDED: A sentinel value. If a suspend call returns this, the coroutine is suspended.
- CoroutineDispatcher: Determines which thread the coroutine runs on (Main, IO, Default).
- Job: A handle to the coroutine's lifecycle. Can be cancelled.
- CoroutineScope: Defines the lifecycle for coroutines. Cancelling the scope cancels all its coroutines.
💻 Code Example
1// Coroutine basics — what's happening under the hood2import kotlinx.coroutines.*34// suspend function — compiler generates a state machine5suspend fun fetchData(): String {6 delay(1000) // Suspension point: releases thread, resumes after 1s7 return "data"8}910// CoroutineScope defines the lifecycle11class MyViewModel : ViewModel() {12 fun loadData() {13 // viewModelScope auto-cancels when ViewModel is cleared14 viewModelScope.launch(Dispatchers.IO) {15 try {16 val data = fetchData()17 withContext(Dispatchers.Main) {18 _uiState.value = UiState.Success(data)19 }20 } catch (e: CancellationException) {21 throw e // NEVER catch CancellationException!22 } catch (e: Exception) {23 _uiState.value = UiState.Error(e.message ?: "")24 }25 }26 }27}2829// Dispatchers explained30// Dispatchers.Main → Main/UI thread (single thread)31// Dispatchers.IO → Thread pool for IO (64 threads default)32// Dispatchers.Default → Thread pool for CPU (cores count)33// Dispatchers.Unconfined → Runs in caller's thread until first suspension3435// Coroutine context elements36val context = Job() + // Lifecycle37 Dispatchers.IO + // Thread38 CoroutineName("fetchData") + // Debug name39 CoroutineExceptionHandler { _, e -> // Error handler40 Log.e("Coroutine", "Failed", e)41 }4243// Structured concurrency — parent-child relationship44suspend fun fetchUserAndPosts(userId: String): UserWithPosts {45 return coroutineScope {46 // Both run in parallel, if one fails both are cancelled47 val userDeferred = async { api.getUser(userId) }48 val postsDeferred = async { api.getPosts(userId) }49 UserWithPosts(userDeferred.await(), postsDeferred.await())50 }51}
🏋️ Practice Exercise
Practice:
- Explain CPS transformation — how does the compiler convert a suspend function into a state machine?
- What is COROUTINE_SUSPENDED and when is it returned?
- Explain the difference between launch and async
- Why is Dispatchers.IO limited to 64 threads? What happens if all are busy?
- Write a coroutine that fetches data from 3 APIs in parallel with a 5-second timeout
⚠️ Common Mistakes
Catching CancellationException — this breaks structured concurrency. Always rethrow it.
Using GlobalScope — no lifecycle awareness, coroutines run forever. Use viewModelScope or lifecycleScope.
Blocking the main thread with runBlocking — defeats the purpose of coroutines, causes ANR.
Not using withContext(Dispatchers.IO) for IO operations — running network/disk on Default dispatcher starves CPU work.
Creating a new CoroutineScope without managing its lifecycle — leads to leaked coroutines.
💼 Interview Questions
🎤 Mock Interview
Mock interview is powered by AI for Coroutines Internals & CPS Transformation. Login to unlock this feature.