Coroutines and suspend functions

Kotlin · Advanced · 15 min

Coroutines are Kotlin's standard for concurrency on Android and the server, replacing callbacks and blocking threads.

A coroutine is a lightweight, suspendable computation. Thousands can run on a few threads because, while one waits for I/O or a delay, it suspends and frees its thread instead of blocking it.

The language provides suspend functions; the library kotlinx.coroutines (add org.jetbrains.kotlinx:kotlinx-coroutines-core) provides builders and utilities.

  • A suspend fun can call other suspending functions such as delay(100). It can only be called from another suspend function or a coroutine.
  • runBlocking { } starts a coroutine and blocks the current thread until it finishes: for main functions and tests only.
  • launch { } starts a coroutine for side effects and returns a Job (join() waits for it).
  • async { } starts a coroutine that computes a value and returns a Deferred<T>; await() suspends until the result is ready.
  • coroutineScope { } groups child coroutines in a suspend function and waits for all of them.

Code inside a coroutine reads sequentially: val user = fetchUser(); val orders = fetchOrders(user). To run independent calls concurrently, start both with async and then await both.

Use delay instead of Thread.sleep inside coroutines: sleep blocks the thread. For blocking APIs (JDBC, file I/O), wrap them in withContext(Dispatchers.IO) { } (next lesson).

Frameworks provide scopes for you: viewModelScope and lifecycleScope on Android, suspend handlers in Ktor and Spring WebFlux.

Example

// build.gradle.kts: implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2")
import kotlinx.coroutines.*
import kotlin.system.measureTimeMillis

suspend fun fetchUser(): String { delay(300); return "Ada" }        // simulated I/O
suspend fun fetchScore(user: String): Int { delay(300); return user.length * 10 }

fun main() = runBlocking {
    val seq = measureTimeMillis {
        val user = fetchUser()
        println("$user ${fetchScore(user)}")       // Ada 30
    }
    println("sequential ~${seq / 100 * 100} ms")    // sequential ~600 ms
    val par = measureTimeMillis {
        val a = async { fetchScore("Ada") }          // both start now
        val b = async { fetchScore("Grace") }
        println(a.await() + b.await())             // 80
    }
    println("concurrent ~${par / 100 * 100} ms")    // concurrent ~300 ms
    val job = launch { repeat(3) { i -> println("tick $i"); delay(50) } }
    job.join()                                     // tick 0 / tick 1 / tick 2
    val jobs = List(10_000) { launch { delay(100) } } // cheap: 10k coroutines
    jobs.joinAll()
    println("done")
}

Remember

  • suspend functions pause without blocking their thread
  • launch for fire-and-forget Jobs, async/await for results
  • Start several async calls before awaiting to run them concurrently
  • runBlocking only in main and tests; use delay, not Thread.sleep

Watch out for

  • Calling Thread.sleep or blocking I/O inside a coroutine on a limited dispatcher, starving other coroutines.

Check yourself

  1. val a = async { f() }.await(); val b = async { g() }.await() with each taking 1 s. Total time?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: kotlinlang.org/docs/coroutines-basics.html