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.
runBlockingstarts a coroutine and blocks the current thread until it finishes. It bridges normal code and coroutines — use it inmainand 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.sleepinside a coroutine. It blocks the whole thread; usedelay.