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

codeTap to expand ⛶
1// Coroutine basics — what's happening under the hood
2import kotlinx.coroutines.*
3
4// suspend function — compiler generates a state machine
5suspend fun fetchData(): String {
6 delay(1000) // Suspension point: releases thread, resumes after 1s
7 return "data"
8}
9
10// CoroutineScope defines the lifecycle
11class MyViewModel : ViewModel() {
12 fun loadData() {
13 // viewModelScope auto-cancels when ViewModel is cleared
14 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}
28
29// Dispatchers explained
30// 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 suspension
34
35// Coroutine context elements
36val context = Job() + // Lifecycle
37 Dispatchers.IO + // Thread
38 CoroutineName("fetchData") + // Debug name
39 CoroutineExceptionHandler { _, e -> // Error handler
40 Log.e("Coroutine", "Failed", e)
41 }
42
43// Structured concurrency — parent-child relationship
44suspend fun fetchUserAndPosts(userId: String): UserWithPosts {
45 return coroutineScope {
46 // Both run in parallel, if one fails both are cancelled
47 val userDeferred = async { api.getUser(userId) }
48 val postsDeferred = async { api.getPosts(userId) }
49 UserWithPosts(userDeferred.await(), postsDeferred.await())
50 }
51}

🏋️ Practice Exercise

Practice:

  1. Explain CPS transformation — how does the compiler convert a suspend function into a state machine?
  2. What is COROUTINE_SUSPENDED and when is it returned?
  3. Explain the difference between launch and async
  4. Why is Dispatchers.IO limited to 64 threads? What happens if all are busy?
  5. 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.