Let's Make Coroutines Work: What is a Coroutine?

Threads block, and blocked threads freeze apps. Here's what a coroutine actually is, what a suspension point does, and why you can run 100,000 of them on a single thread.

Let's Make Coroutines Work: What is a Coroutine?

In order to understand coroutines, we first have to understand what a Thread is.

The Problem: Single Threads#

If we want to prepare a meal, such as rice and chicken, there are a few ways to go about it.

Let's imagine that cooking the rice takes 15 minutes, and cooking the chicken takes another 20 minutes. If we do this sequentially — waiting for the rice to finish before starting the chicken — we will spend 35 minutes preparing the meal.

If we run the following code in a single thread, for example:

Kotlin
println("Making a request...")

val rice = api.cookTheRice() // The thread is blocked here for 15 minutes

println("Request response: $rice")

Each line is executed one by one, sequentially. This means that while we are waiting for the rice to be cooked, the thread is completely blocked. It can't do anything else.

If this happens on our main UI thread, our app freezes, and the user gets an Application Not Responding (ANR) error.

The Old Solution: Multithreading#

Multithreading allows us to have multiple, independent execution flows. When the rice is cooking, and the CPU isn't actively using resources, we can use another thread to start cooking the chicken simultaneously.

Kotlin
// Thread 1
println("Starting the rice...")
val rice = api.cookTheRice()

// Thread 2
println("Starting the chicken...")
val chicken = api.cookTheChicken()

This gives us a huge performance boost just by minimizing the amount of time our CPU is sitting idle.

However, threads are heavy. They consume a lot of memory. If we try to fire up 10,000 threads to handle a massive list of tasks, our app will quickly run out of memory and crash.

The Modern Solution: Coroutines#

Coroutines cooking analogy

Coroutine behaviour looks very similar to threads; instructions run sequentially until they reach a suspension point.

Kotlin
fun cookingChicken() = runBlocking {
    println("Before start cooking...")
    delay(1000) // This is the suspension point, aka: we are cooking
    println("Cooking chicken finished")
}

A suspension point is any instruction that requires waiting for an outcome, such as a network call finishing, JSON parsing, or uploading a file.

When a coroutine reaches a suspension point, it uses a concept named Continuation to save its internal state.

This allows the coroutine to pause, step off the thread, and let other coroutines use the same thread to do their work. Once the result is ready, the original coroutine resumes right where it left off.

And this brings amazing advantages:

  • Extremely lightweight: you can run 100,000 coroutines on a single thread without memory issues.
  • Simple to read: you can write asynchronous code that looks totally synchronous.
  • Smart thread switching: the flow just pauses and resumes itself, seamlessly executing the right type of work on the right thread.

A thread is a resource you allocate. A coroutine is a piece of work you schedule. Once that distinction clicks, structured concurrency stops feeling like magic.

Wrapping up#

Coroutines recap

Coroutines give us the power of concurrency without the high cost of traditional threads. They keep our code clean, our apps fast, and our main threads unblocked.

In the next post we'll dive into concurrency vs. parallelism — the difference between managing multiple tasks at once (concurrency) and actually executing them at the exact same time across multiple CPU cores (parallelism).

Until then, let's make this work.

Keep reading

More breakdowns

Android Architecture

Let's Make SOLID Work: The Principles and Patterns Behind Android Architecture

AsyncTask died, Fragments were rewritten twice, XML gave way to Compose. What survived every migration was the shape of the code underneath. Here are the five principles and nine patterns that decide it.

· 6 min read