A deep dive into Kotlin Coroutines

Vinicius Viana
15 min readMar 21, 2022

--

A deep dive into Coroutines

A college friend invited me to do a presentation in a live stream and the theme I choose was Coroutines, and the reason was that I always wanted to understand how it works under the hood and the history involving the creation of coroutines. Why I was interested in coroutine history? Well, I made some games using Unity and there are Coroutines in Unity, so I wanted to understand more of this concept.

Origins

The concept of coroutines was conceived in 1958 by Melvin Edward Conway and he was the first to apply the concept in an Assembly Program. Years later, in 1963, he published an article called “Design of a Separable Transition-diagram Compiler” and talks about the creation of a Cobol Compiler that has the design property of separability.

What does he mean about separability? A program organization is separable if is divided into processing modules that communicate with each other according to some restrictions. And if the processing modules communicate following these restrictions, each module may be made into a coroutine.

And what means to be made a Coroutine? It means that each module communicates with adjacent modules as if they were input or output subroutines. Basically, they all think they were the master program, when in fact there is no master's program.

You probably are a little confused right now and that's okay, Coroutine is a difficult concept to understand, so I will try to explain the best I can.

When you call a function B from function A, A has to wait until B is finished to continue executing, this is what we call functions non-cooperative. And this control of function B over A is made by the OS, B has to finish, so the OS can give control back to A. To not have this situation where A needs to wait for B, you need to make B run in a different thread and now A doesn't need to wait.

Coroutines are functions that are cooperative, functions where the control of how runs are made by the “user” and not the OS, so you can have the same effect of using different threads without really needing more the one. And to have that effect, the control is transferred from one function to another in a way that the exit point from the first function and the entry point to the second function is remembered, without changing the context. That way, you can have the effect that you using multi threads when you actually using one, and because of that you often hear that coroutines are light threads.

You will understand more of this explication at the end of the article.

Kotlin Coroutines

Coroutine is an old concept, so you can expect to see the concept being applied in various languages, like C#, Python, Swift, Scala, Javascript, C, C++, and many others. So is not surprising to see this concept being used in Kotlin, a new language that uses the best ideas of various languages.

The best way to start an explanation is using a “Hello world” example.

Hello World Example

This code will create a coroutine using GlobalScope.launch{} and inside of that coroutine, will delay the execution for 1 second and will print “World!”. Outside of the coroutine, the program will print “Hello,” and will make the thread sleep for 2 seconds.

The output of the code will be:

Hello, // and after one second pass
World! // it will print this

A quick explanation of the code:

GlobalScope is Coroutine Scope and with that, we can create coroutine using the function launch, and this function needs one anonymous suspend function, that is the {}. After creating the coroutine we call the delay function, which also is a suspend function, and as the name says it will delay the function for 1 second. After one second is passed the delay function is finished, and the anonymous suspend function gets resumed and continues to execute.

I put the Thread.sleep() at the end of the code because, without that, the program would finish executing because the delay function doesn't block the main thread, it only suspends the function.

And how I would code this using only threads?

The only two things I would change is GlobalScope.launch{} for thread{} and delay() for Thread.sleep(), so what do I gain by using coroutines? Because the code difference between using multiple threads is really small. As I said before in this article, threads are something heavy, like 2MB of Memory and when coding, we want to make our code use a little amount of memory because we want to scale as much as possible. And that is even more true when coding to android development, where the memory is small and others apps are fighting for the memory and when an app uses a lot of memory, the user experience becomes worse and will eventually lead to the user uninstalling your app.

If you want to check this difference of memory used by Threads and Coroutines, try to make a program that creates 100 thousand threads and one that creates 100 thousand coroutines. The first program will lead to OutOfMemory Exception, and the second problem will work fine.

Something you always have to have in mind when using a Coroutine is that Coroutine is not equal to a thread, it runs on a thread, but is not a thread.

So, in the “Hello World” example, what thread was that coroutine running?

If you run this code, the output will be this:

In Thread Thread[main,5,main] // printThread() outside coroutine
Hello,
World!
In Thread Thread[DefaultDispatcher-worker-1,5,main] // printThread() inside coroutine

The coroutine is executed on a worker thread, and they usually run on a worker thread, but you can make the coroutine be executed in other threads.

Suspend Function

I talk about delay and the anonymous function in launch {} being a suspend function and what that means?

Usually, on kotlin you will write a function like this:

The function can have arguments and returns:

And how can I transform the makeSomething() function into a suspend function?

The only thing I need is the suspend modifier.

What is a modifier? Is something that you add to the function to modify her, like private/public/protected. These other modifiers you already know what they do, but what does the suspend modifier do?

He informs the compile that this function can be suspended and resumed

In the final part, you will understand how he does, but for now, the only thing you need to remember is that when adding the suspend, the compiler will know that this function can be suspended.

One thing before continuing, suspend functions can only be called within others suspend functions or coroutines.

Why use suspend functions?

You can be thinking: “Okay, I can transform functions in suspend functions that can be suspended, how does that help?”. Well, let me give you this example of something that probably you already used or have seen before:

In this code, I want to load some movies from an API, but before I need the token that authorizes my request. I’m not using other libraries like Rxjava, I will be doing this only in pure kotlin or in other words, callbacks. Assume that the code that actually does the requests is the basic Retrofit Call using enqueue and the return of the request is given within a callback.

Because I am already using a callback when using Retrofit Call, I will need to access the response using a Callback, and because I need a callback to know the response of makeLogin(), I need a callback to know the response of loadMovies() and if was need to add another request, would be required another callback. This is a simple example of using a callback, but when using it in a more complex situation it is easy to run into the callback hell.

I will show now how to improve drastically this code using Coroutines.

The first thing we need to do is to make makeLogin() and loadMovies() into suspend functions.

The someFunction() will complain that she is not a suspend function and loadMovies() cant be called within her. To fix that, we will be creating the coroutine inside this function and putting loadMovies() inside.

Now, the callbacks are not more necessary, we can just remove them and make the return of token and movies like we do normally in a function.

I had asked you to assume that the code for doing the actual request was retrofit Call and enqueue, but now assume that I’m using the Deferred<> or suspend implementation of Retrofit.

And now, after using the suspend function, my code is cleaner and easier to learn, makeLogin() and loadMovies() are async functions, they can take seconds to finish but they look like non-async functions. When makeLogin() is called, the anonymous suspend function of launch {} will be suspended and when makeLogin() is finished she will be resumed and the same thing will happen when loadMovies() is called. If inside of loadMovies() was another suspend function, she would be suspended until the other function is finished.

How the suspend function can be suspended and be resumed returning to the same point that before? This I will explain at the end of the article, for now, just know that this happens.

CoroutineScope

I talked before that we need a CoroutineScope to launch a coroutine, but what is a CoroutineScope? As the name suggests, is the Scope of the Coroutine, but how we can use this CoroutineScope?

Imagine that we have a project that the main purpose is to make math calculations and because calculations are expansive things to compute in the UI Thread/Main Thread we want to use Coroutines to make the calculations. To help organize our code, we can create a coroutine scope called mathScope and create all of the coroutines that will do math calculations using this scope.

And by using a specific scope, if I wanted to stop all the calculations from happening in my project, I can do this:

We can cancel the mathScope and if I try to launch a new coroutine from this scope, nothing will happen, no coroutine will be launched. Off course that the cancel() is not magical, it will not stop any coroutine that is already running, not automatically, to make this happen we can use:

And by using this, we can continue to do the operation only if the scope is active. Using different scopes is very useful because we can cancel and stop launching new coroutines when not needed anymore.

On Android, we have the lifycecleScope and viewModelScope, one is to do things on the Activity/Fragment and the other to do things on the ViewModel.

How to create?

A coroutine scope is made by multiple parts, the most important being the Job and Dispatcher, there are other parts like a CoroutineExceptionHandler, but they are not used often.

Job

As the name says, is a job to be executed, has a life cycle and it is cancelable, it is the Job that permits the CoroutineScope to be cancelable. Every coroutine created by the CoroutineScope inherits the Job and if the parent job is canceled, she cannot be executed. You can prevent that if you set a new Job when using launch {}.

That way, we can override the parent Job and if the parent is canceled, the coroutine works fine.

Before continuing, let me say two things. First, the launch { } function also returns a Job, so you can cancel that specific coroutine if you want.

And the second thing, when using the Job() if one his children fail, like throw an exception, the parent job is also canceled. So, if you have a requestScope and you create a coroutine that makes a request and he fails, it will throw an Exception(like HTTPException on Retrofit) and the requestScope will get canceled. To not have this problem, use SupervisorJob(), a cancellation of children will not affect the parent or other children.

Dispatcher

The dispatcher says on what thread the coroutine will run, if when creating a CoroutineScope we only pass the Job(), it will use the Dispatchers.Default, but there are other dispatchers. I will show the most used dispatchers:

Dispatcher.IO: used to make IO, like network or reading a file or accessing a database.

Dispatcher.Default: used normally to do CPU intensive operations

Dispatcher.Main: a dispatcher that only exists in the Android version of a coroutine, it runs the coroutine on the main thread.

When we launch a coroutine from a CoroutineScope it inherits the Dispatcher, but we can override and do some cool stuff, like this:

It is cool, but because launch doesn't return the value of the coroutine, we can still bump into the callback hell problem, so who to fix it? To help us change the Dispatcher more easily, we can use the withContext() function:

That way we can get the result in a better way and avoid the callback hell problem, but we can do better.

Async/Await

Kotlin is a language that was created by adding the best thing of multiple languages, in other languages like C#, we have the async/await keywords that are used to make async operations. In Kotlin they are not keywords but functions that are used to make async operations.

Inside a CoroutineScope, we have the function called async

And the async function returns something, she returns a Deferred<T>

Deferred is a class that inherits Job, it is basically a Job with a result. You will use it when you need to perform a coroutine that returns a result. Exactly the thing that we need in the loadMovies example right? Yes, but I will not show the same example, I will show something a little different, this time we have two requests to make.

And with withContext

And with async

When using the async, to get the result we need to call await() to get the result. By calling await(), it will suspend the function inside async without blocking the thread and when the function finishes executing, it will resume and returns the result on await(). You can see that when using async we can set the dispatcher, this is very useful.

You probably are thinking that the withContext and async examples are very similar, so why use one instead of the other? If the first loadMovies requires a 2 second period to finish executing and the second loadMovies requires also 2 second to finish, the requestScope.launch code will take a little more than 4 seconds to finish. This is the case when using both examples, but we can do something about this when using async, if instead of calling await right from the start we only call when showing the moviesGenre, we can do this:

By calling the await() in the same line, the two async will run simultaneously, thus making the total time of executing being only 2 seconds. Now, we have a code that does not block the thread and that runs simultaneously, this is the beauty of async and await functions.

How does it work under the hood?

After seeing all this, you probably are wondering how a function can be suspended and resumed? How does the Compile know that this function can be suspended? All this will be responded on this section of the article.

To understand more of how the compiler knows that a suspend function can be suspended, we need to look at our code become when is in bytecode and how can we do that? Using the Android Studio.

Create a file and put this code in the file:

With this file open on Android Studio, go to Tools -> Kotlin -> Show Kotlin Bytecode

When clicking on Show Kotlin Bytecode, it shown this:

Even a simple function declaration in bytecode is something difficult to understand, but we can Decompile this bytecode into Java, that way we can understand more of the thing that is happing inside the Coroutine. So, click on the Decompile button.

The output will be this, and we can easily identify our loadMovies function in this Java. Our function now has an argument of the type Continuation, where does this argument come from? You could be thinking that this argument is something that all functions on bytecode have, but you are wrong. Try to add a normal function that has no argument and the result is going to be this:

The other function doesn't have an argument, so where does the Continuation come from? This is how the suspend function is suspended and resumed, he is a generic interface of a callback.

In the end, you will not have to create callbacks, but the compiler will. Of course, the compiler does it in a more performative way, he uses a state machine.

The Continuation is an interface that has the CoroutineContext and two functions, resume and resumeWithException that are used to resume the suspend function.

In the example, there is no suspend function inside loadMovies, so the continuation has no use, but if we get an example that has other suspend functions, we will see the continuation being used.

In this example, setupMovies has other functions inside, and if we do the same thing that before, using IntelliJ to see the code that is generated, we will see how the continuation is used. Because it is a code difficult to read and understand, I will write the code using kotlin.

If you generate the code, will you see a switch statement, so let’s start with this:

For every suspend function, we will add a branch to the switch statement and one more to resume the setupMovies function, so our switch will have 4 branches and an else just in case we get a label with a different value.

In the first branch, when label equals 0, we call the makeLogin() function and we assign the return value to the variable token. And in the second branch, when label equals 1, we use that variable token and pass it as an argument to the loadMovies() function. This should not be possible, because the variable token should only exist inside the first branch, but how can we access it from the second branch? Using the Continuation interface.

Every branch in the switch is a state of the State Machine, but where is the State Machine? He is an implementation of the Continuation interface:

The state machine will have the variables that need to be stored, like token, movies, result, and label.

  • The label variable is the state in which the State Machine is.
  • The token and movies variable is the variable that gets the result of one the state of the State Machine.
  • The result variable is the result from the previous state, beginning with the value null.

And the state machine also has a function called invokeSuspend, this function is passed down to the next suspend function to be called with the result of that function.

Now, we add the code of the state machine to the code of before:

Now, we are gonna analyze the code step by step.

At the start of the setupMovies(), if the completion argument is not a SetupMoviesStateMachine then we will create a SetupMoviesStateMachine passing the completion as an argument. If it is a SetupMoviesStateMachine, we use the completion argument.

When calling the setupMovies for the first time, the label will be 0, so the branch that will enter in the switch is where we call the makeLogin() function. Remember that for every suspend function the compiler adds an argument, so we need to pass that argument to the makeLogin and we pass the variable continuation. Why do we pass the continuation? Because when the makeLogin is resumed she will call us using the invokeSuspend function passing her result so that we can call again the setupMovies function passing the continuation with the result saved.

Because at the end of the first branch of the switch we change the label to 1, when invokeSuspend calls setupMovies again, now the second branch will be called. In this branch, we will assign a value to the variable token of the state machine using the result variable and after that, we will call the loadMovies passing the token and continuation. Again, we pass the continuation so that the loadMovies can call our invokeSuspend function so that we can get the result and call again setupMovies to the next branch be called.

In the third branch, we do basically the same thing.

On the fourth branch, we just resume our setupMovies function. If the setupMovies had something after the printMovies that is not a suspend function, that something would be in this fourth branch.

In this example, what is not a suspend function after printMovies is the println function, so the fourth branch would be something like this:

When continuation.resume() is called, the suspend function is resumed, and just like the loadMovies calls the setupMovies after is resumed, setupMovies could call another suspend function when resumed.

What you probably are thinking is, what happens when an error occurs? Where is the error handling? I purposely not added the error-handling code, not because it is complex, but because I wanted you to think about how it is implemented. It is very simple, we just need to add one line for every branch of the switch:

We just need to add the throwOnFailure(continuation.result) and the error handling will be done.

Wonderful isn't it? I think this is wonderful, the concept of suspending and resuming a function always looks like magic, but now understand that under the hood, the compiler will use callbacks with state machines and for us, programmers, the code will look like a normal synchronous code.

Conclusion

Coroutine is something very old and that is being implemented in various languages, including kotlin. It is a powerful tool that is easy to use and can improve our code in a lot of ways, but a lot of times is a difficult thing to understand.

I hope that you enjoyed this article and that he helped you to understand more of Kotlin Coroutines, especially how works under the hood.

Have a great day!

Sources

--

--