Using Coroutines? Then Keep These in Mind — Part 1 of 2

Masoud Fallahpour
4 min readJun 15, 2022
Photo by Maria Ionova on Unsplash

When using Kotlin, sooner or later, you come across coroutines. In two articles we’re going to review the most fundamental things you need to know when using coroutines. This is the first part. You can read the second part here.

For the rest of this article, I assume that you know the basics of coroutines.

Basics

In this section, we’ll go through a couple of basic good-to-know things about coroutines.

Think of coroutines as light-weight threads

Coroutines are less resource-intensive compared to threads. When dealing with a handful of coroutines/threads this difference may not be obvious but when dealing with say 100000 them you will witness the difference. Code that exhausts the available memory of JVM when using threads can be expressed using coroutines without reaching the memory limit. For example, the following code launches 100000 coroutines that each wait 3 seconds and then print a message while consuming a reasonable amount of memory.

Writing the above piece of code using threads is as follows:

Running the above code consumes too much memory and will probably throw an OutOfMemoryError exception if your machine does not have a huge amount of RAM available.

Each coroutine has a scope

Each coroutine has a scope (which is of typeCoroutineScope). In other words, you cannot start a coroutine without specifying its scope. The scope determines the lifetime of the coroutine. Also, the scope allows the so-called structured concurrency.

As an example consider Android’s ViewModel class. A ViewModel has a coroutine scope named viewModelScope and its lifecycle is tied to the ViewModel. Being tied to the ViewModel’s lifecycle means that if you launch coroutines using the viewModelScope and the ViewModel is destroyed then all those coroutines are canceled automatically and there is no need to be worried about resource-wasting, memory leaks, etc.

Each coroutine has a context

Each coroutine has a context (which is of type CoroutineContext). Coroutine context is a set of elements. Think of the elements as the properties of a coroutine like its name, dispatcher, job, etc. Some of the classes/interfaces that implement CoroutineContext are:

  • CoroutineName: User-specified name of coroutine. This name is used when debugging coroutines.
  • CoroutineDispatcher: Determines the dispatcher which is used to run the coroutine.
  • Job: Provides a couple of useful operations that can be done on a coroutine like canceling it.

When you create a coroutine you can provide its context. If you do not specify the context of a coroutine, the context will have a default value of EmptyCoroutineContext.

As an example, the following code creates a coroutine with a custom name and the IO dispatcher.

Take a look at here if you want to get more familiar with the coroutine context.

A coroutine is not bound to a specific thread

In general, a coroutine is not bound to any particular thread. It may suspend its execution in one thread and resume in another one. This means that there is a pool of threads available and at each point in time a potentially different thread from this pool could be responsible for running the coroutine code.

In order to see this behavior in action, we will use the Unconfined dispatcher. Consider the following code:

If we run the above code, the output will be like the following:

Before delay. Thread name: main
After delay. Thread name: kotlinx.coroutines.DefaultExecutor

This means that the coroutine started its execution on a thread named main. Then it got suspended (by calling delay). After suspension, it continued its execution on another thread named kotlinx.coroutines.DefaultExecutor.

Parent-child Relationship

This section will cover some notes about the parent-child relationship between coroutines.

Child coroutines

When a coroutine is launched in the scope of another coroutine, the new coroutine becomes the child of the parent coroutine. Take a look at the following code.

In the above code, we launch two coroutines (child1 and child2) in the scope of another coroutine (parent). This makes child1 and child2 the children of parent. When a coroutine becomes the child of another coroutine a couple of things happen. One such thing is that the child coroutine inherits the CoroutineContext of its parent.

Parent coroutine always waits

A parent coroutine always waits for the completion of all its children. A parent coroutine does not have to explicitly track all the children it launches, and it does not have to call join() on the Job of each child to wait for their completion.

Running the code above gives the following output.

Now processing of the request is complete
request: I'm done
Coroutine 0 is done
Coroutine 1 is done
Coroutine 2 is done

In the above code, we launch 3 coroutines inside the request coroutine. This effectively means that those 3 coroutines become the children of therequest coroutine. In order to wait for all the children to finish we just call request.join() and the request coroutine will wait for all its children to finish.

That’s it for the first part. You can read the second part here!

--

--

Masoud Fallahpour

Software engineering @ Klarna. Software engineer, *nix lover, curious learner, gym guy, casual gamer.