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 to cook the chicken, we will spend 35 minutes preparing the meal.

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

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.

// 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 behaviour looks very similar to threads; instructions run sequentially until they reach a suspension point.

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 (like a delay or a network request), 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.

Wrapping up

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. We’ll break down 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, check out the video version of this post on the YouTube channel, and let’s make this work!

Leave a Reply

Your email address will not be published. Required fields are marked *