Phase 6 · Coroutines & AsyncModule 30~42 min read

Coroutines Fundamentals

Write asynchronous, non-blocking code with suspend functions and coroutine builders.

What you'll learn

Welcome to Phase 6. Coroutines are Kotlin's answer to asynchronous programming — a way to write non-blocking, concurrent code that reads like ordinary sequential code. They power modern Android apps and high-throughput servers, and they're one of the best reasons to choose Kotlin.

By the end you'll be able to:

  • Explain the difference between blocking and suspending
  • Write and call suspend functions
  • Start coroutines with launch, async, and runBlocking
  • Run work concurrently and collect results with await

Blocking vs suspending

A traditional call like Thread.sleep(1000) blocks — it holds the thread hostage doing nothing for a full second. Threads are expensive, so blocking thousands of them doesn't scale. A coroutine delay(1000) instead suspends: it releases the thread so other work can run, then resumes later. The code still reads top-to-bottom, but nothing is wasted.

First.kt
import kotlinx.coroutines.*

fun main() = runBlocking {          // bridge into the coroutine world
    println("Start")
    launch {                        // start a coroutine, don't wait for it
        delay(1000L)                // suspend 1s WITHOUT blocking the thread
        println("World!")
    }
    println("Hello,")               // runs while the coroutine is delayed
}

Key idea

A coroutine is a suspendable computation — far lighter than a thread. You can run hundreds of thousands of coroutines on a handful of threads, because a suspended coroutine costs almost nothing. Coroutines live in the kotlinx.coroutines library, added via Gradle.

suspend functions

The suspend modifier marks a function that can pause and resume. Inside it you can call other suspend functions (like delay) as if they were normal — the compiler handles the pausing for you. The catch: a suspend function can only be called from a coroutine or another suspend function, which is how Kotlin keeps async code honest.

Suspend.kt
import kotlinx.coroutines.*

// A suspend function can pause and resume. It can only be called from a
// coroutine or from another suspend function — never from ordinary code.
suspend fun fetchUser(): String {
    delay(500L)                     // pretend this is a network request
    return "Ada"
}

fun main() = runBlocking {
    println("Fetching...")
    val user = fetchUser()          // suspends here; the thread stays free
    println("Got $user")
}

Note

Marking a function suspend doesn't make it run on a background thread by itself — it simply grants it the ability to suspend. Where it runs is decided by the dispatcher, which you'll meet in the next module.

launch, async & runBlocking

Coroutines are started by builders. runBlocking bridges the normal world and the coroutine world (great for main and tests). launch fires off a coroutine that returns no result — "fire and forget." async starts one thatdoes return a result. Starting two async blocks lets their work overlap:

Async.kt
import kotlinx.coroutines.*
import kotlin.system.measureTimeMillis

suspend fun loadA(): Int { delay(1000L); return 10 }
suspend fun loadB(): Int { delay(1000L); return 20 }

fun main() = runBlocking {
    val time = measureTimeMillis {
        val a = async { loadA() }   // starts right away
        val b = async { loadB() }   // starts right away — runs concurrently
        println("Sum: ${a.await() + b.await()}")   // wait for both results
    }
    println("Took ~${time}ms")     // ~1000ms, not 2000ms
}

Tip

The magic above is concurrency: two one-second tasks finish in about one second, not two, because they run at the same time. Use launch when you don't need a result and asyncwhen you do.

Job, Deferred & await

Every builder returns a handle. launch returns a Job you can join() (wait for) or cancel(). async returns a Deferred<T> — a Job that also carries a future result you retrieve with await(). These handles are how you control a coroutine's lifecycle:

Job.kt
import kotlinx.coroutines.*

fun main() = runBlocking {
    val job = launch {              // launch returns a Job handle
        repeat(5) { i ->
            delay(200L)
            println("working $i")
        }
    }
    delay(500L)
    println("cancelling")
    job.cancel()                    // stop the coroutine
    job.join()                      // wait until it has finished stopping
    println("done")
}

Recap & quick check

Key takeaways

  • Blocking holds a thread idle; suspending frees the thread to do other work and resumes later.
  • A coroutine is a lightweight, suspendable computation — you can run huge numbers of them cheaply.
  • suspend functions can pause and resume; they may only be called from a coroutine or another suspend function.
  • runBlocking bridges into coroutines; launch is fire-and-forget; async returns a result.
  • launch returns a Job (join/cancel); async returns a Deferred whose value you get with await().

Quick check

1. What is the key difference between delay() and Thread.sleep()?

2. Where can a suspend function be called from?

3. Which builder should you use to get a result back concurrently?

4. What does launch return?

5. Why do two 1-second async tasks finish in ~1 second?

You can now write asynchronous code that reads sequentially. Next we'll control where coroutines run — threads, thread pools, and the main thread — with coroutine context and dispatchers.