Skip to content
AgileLogics
7 min read

Coroutines: The Basics

Prerequisites: Lambdas and higher-order functions, Gradle basics

Some work takes time: network requests, database queries, reading large files. Coroutines let a program wait for that work without blocking a thread, so an app stays responsive and a server can handle many requests with few threads.

Real-world analogy: a chef with several dishes on the stove. While one simmers, the chef doesn't stand and stare at it — they chop vegetables for the next. Coroutines let your code "step away" the same way.

Coroutines come from the kotlinx.coroutines library:

// build.gradle.kts
dependencies {
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2")
}

suspend functions

A suspend function can pause without blocking the thread, and resume later. delay is the suspending version of "wait":

import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking

suspend fun fetchUser(id: Int): String {
    delay(500)          // stands in for a network call
    return "User $id"
}

fun main() = runBlocking {
    println("Fetching...")
    val user = fetchUser(1)
    println(user)
}
Fetching...
User 1

The code reads top to bottom like ordinary code, even though it waits in the middle.

  • Suspend functions can only be called from other suspend functions or from a coroutine.
  • runBlocking starts a coroutine and blocks the current thread until it finishes. It bridges normal code and coroutines — use it in main and in tests, but not inside apps, where blocking a thread defeats the purpose.

launch — start work that runs alongside

import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking

fun main() = runBlocking {
    launch {
        delay(200)
        println("World!")
    }
    println("Hello,")
}
Hello,
World!

launch starts a new coroutine and immediately carries on, so "Hello," prints first. runBlocking still waits for its child coroutines before finishing — that's structured concurrency, covered in the next lesson.

Coroutines are lightweight

Coroutines aren't threads. They're cheap enough to start by the hundred thousand:

import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import java.util.concurrent.atomic.AtomicInteger

fun main() = runBlocking {
    val finished = AtomicInteger()
    repeat(100_000) {
        launch {
            delay(1000)
            finished.incrementAndGet()
        }
    }
    // runBlocking waits for all 100,000 children
    println(finished.get() == 0)
}
true

The println runs straight after the loop, before any coroutine has finished its one-second delay; then runBlocking waits about one second for all of them. With 100,000 real threads, most machines would run out of memory.

Common mistakes

  • Calling a suspend function from normal code: Suspend function 'fetchUser' should be called only from a coroutine or another suspend function.
  • Using Thread.sleep inside a coroutine. It blocks the whole thread; use delay.