Coroutines basics

Maciej Nowak
Fandom Engineering
Published in
12 min readNov 10, 2020
Photo by Michał Parzuchowski on Unsplash

We developers are constantly on a lookout for new solutions, the best approaches so no wonder the world is changing and asynchronous programming is changing with it. At Fandom we do the same: we want to follow and apply the best solutions for specific problems. And that’s what we did for one of our latest projects. We decided to use Kotlin Coroutines in our Android app, not only because they are native (less dependencies, loosely coupled) but especially their usage has certain benefits.

It doesn’t matter if you write mobile or desktop or even server-side applications, there could be a place for Kotlin Coroutines in your code. But what actually are they? How do they work? What problem do they solve? Where and when should they be used? In this article I would like to answer all of these questions. If you have never heard about them or you know a little this blog post is for you.

What are coroutines?

Coroutines by definition are light-weight threads. So what does it actually mean? We can divide the name into two parts: coroutines = co + routines. So we can think about coroutines as cooperative (co) functions (routines) working on threads. In this context, cooperative means that functions can work together on shared tasks.

fun task() {
functionA(1)
}
fun functionA(state: Int) {
when (state) {
1 -> {
// work on UI thread
functionB(1)
}
2 -> {
// work on UI thread
functionB(2)
}
}
}
fun functionB(state: Int) {
when (state) {
1 -> {
// work on IO thread
functionA(2)
}
2 -> {
// work on IO thread
}
}
}

In the above example task is performed by cooperation of functionA and functionB. It’s worth noticing that, at some point, there is a need to switch the thread. Coroutines allow us to easily write multithreaded asynchronous code in a synchronous way. In other words, coroutines are a sequence of subtasks executed in specific order.

Are they threads?

As mentioned above “coroutines are light-weight threads”, but they are not threads at all! They can run in parallel, communicate and wait for each other just like threads, but, unlike them, they are very cheap in terms of performance. We can think about coroutines as a framework of efficient work on threads.

fun runALotOfCoroutines() = runBlocking {
repeat(100_000) { // launch 100k coroutines
launch {
// some work
}
}
}

At this point you only need to know that runBlocking is a coroutine builder function that blocks the current thread until all tasks in a coroutine are completed. This one could be useful for writing tests that need to be paused or learning examples just like this one. If some keywords are not clear, don’t worry, we will talk about them later.

What problem do they solve?

Okay, now we know what coroutines are and their biggest advantage versus threads but where is the place to use them? It’s time to look closer at ways of running asynchronous code based on an example.

fun loadAndShowDataCallback() {
loadData { data -> // run on IO thread
showData(data) // pass callback
}
}

The traditional model of creating asynchronous code based on callback methods is subject to various difficulties. In the case of execution of concurrent dependent tasks, the problem of communication between tasks may arise, which forces finding a way to synchronize the results, thus increasing the complexity of the code. Another problem is the nesting of callback methods, which in turn means that tasks are actually performed synchronously (pyramid of doom), in the result extending their processing time.

fun loadAndShowDataRx() {
loadData() // observable
.subscribeOn(Schedulers.io())
.observerOn(AndroidSchedulers.mainThread())
.subscribe { data ->
showData(data)
}
}

There is also a reactive approach offered by RxJava, a powerful, complex library which has been a rescue for many for a long time. It gets rid of nested callbacks by implementing the reactive programming paradigm.

// don’t do this at home, just to simplify an example
fun loadAndShowDataCoroutines() {
GlobalScope.launch(Dispatchers.IO) {
// launch coroutine in IO thread
val result = loadData()
withContext(Dispatchers.Main) {
// change context to Main thread
showData(result)
}
}
}

Coroutines make concurrent tasks much easier by changing the style of writing asynchronous code sequentially. Thanks to this approach, the code is more readable and understandable and task management becomes easier. In addition, one thread can handle many coroutines simultaneously, which translates into a significant increase in efficiency. Coroutines allow us to avoid the callback hell and are a good alternative to RxJava.

Suspend function

One of the key concepts of coroutines is the idea of a suspend function. This kind of function can suspend its operation until later resume without blocking the thread (for example waiting to resume for the end of another function result). The cost of suspension in relation to blocking the thread is much lower. A suspend function must be called inside coroutine or in another suspend function on any thread.

fun authorizeAndShowData() = runBlocking { 
val isAuthorized = authorize()
// wait here until authorize finish
if (isAuthorized) {
val result = loadData()
// wait here until loadData finish
}
}
// declare suspend function just by suspend keyword
suspend fun authorize(): Boolean {
// make some network calls
return true
}
suspend fun loadData(): String {
// fetch data
return "result"
}

Launch and Async

Creating and executing a coroutine can be done by one of the builder functions like: runBlocking, launch, async (you could have noticed some of them before). Actually, in practice, coroutines are mostly created by coroutine builders. Builder functions are not suspend functions, they just create a new coroutine. Also, coroutines are built and work within structured concurrency which means that a parent has the ability to influence their children.

fun launchAndJoin() = runBlocking {
val job = launch {
val result = suspendFun()
// wait for result
}

// wait here until a coroutine referenced by job finish
job.join() // suspend function itself
// do some further work
}
fun launchAndCancel() = runBlocking {
val parentJob = launch {
val jobA = launch {
// some work
delay(100)
}
val jobB = launch {
// some work
delay(300)
}
}
// do some time consuming work
delay(200) // suspend function itself

// cancel all cancellable work of coroutine and its children
// referenced by parentJob
parentJob.cancel()
// only jobA has finished
}

launch is used for fire and forget tasks in which the result is not expected. It returns a handler to its coroutine as a Job type object, thanks to which it is possible to manually manage the state of the coroutine. join method blocks the related coroutine until all its tasks are completed, while cancel just cancels the whole coroutine.

fun runSuspendFun() = runBlocking {
val resultA = suspendFunA()
val resultB = suspendFunB()
// wait for the results of both suspend functions
val finalResult = "result: $resultA $resultB"
// it takes about 400ms to get here
}
fun launchAsync() = runBlocking {
val deferredA = async {
val resultA = suspendFunA()
return@async resultA
}
// deferredA has already started

val deferredB = async(start = CoroutineStart.LAZY) {
// can be lazy started
val resultB = suspendFunB()
return@async resultB
}
// deferredB waits to start by calling start or await
deferredB.start() // to avoid sequential behaviour start here
// wait for the results of both coroutines
val finalResult =
"result: ${deferredA.await()} ${deferredB.await()}"
// it takes about 200ms to get here
}
suspend fun suspendFunA(): String {
delay(200)
return "result A"
}
suspend fun suspendFunB(): String {
delay(200)
return "result B"
}

async, similarly to launch, allows for parallel execution of tasks. However, it returns an object of the Deferred (extends Job) type which is a promise of a future result. So when tasks are independent of each other and return some result, use async to get things faster. await suspends further execution of the statement until a result is obtained.

Dispatcher

Coroutine execution can be confined to a specific thread, run unconfined or dispatched to a thread pool. CoroutineDispatcher object helps to decide on which thread a coroutine runs. To do that, simply just use one of the defined dispatchers in Dispatchers class.

fun launchCoroutinesOnDifferentThreads() = runBlocking {
launch { // actually Default is used implicit
// expensive calculations
}
launch(Dispatchers.IO) {
// network call
}
launch(Dispatchers.Main) {
// update UI
}
launch(Dispatchers.Unconfined) {
// work on thread inherited from parent runBlocking
delay(100) // suspend point
// back to work on different thread inherited
// from suspend point
}
launch(newSingleThreadContext("OwnThread")) {
// work on newly created own thread
}
}

Default is a default dispatcher that can run on a shared pool of threads and should be used for expensive CPU work such as calculations. IO is mainly used for some I/O operations like network call, database or file access. Main runs on the main thread — for Android it’s UI thread. Unconfined starts a coroutine in the caller thread and, after the first suspension, it resumes on the thread defined by the suspending function that was invoked. newSingleThreadContext creates a context with a dedicated thread, which is very expensive so use it carefully.

Context

Every coroutine must be executed in some context represented by a value of the CoroutineContext type, which is a set of rules and configuration that defines how coroutine is executed. It can also be a combination of objects of different context types ( CombinedContext). CoroutineContext consists of objects of Job, CoroutineDispatcher and CoroutineExceptionHandler types and can be passed explicitly or derived implicitly from the scope in which it is being executed. If you want to stay in the same coroutine but do work on a different thread, just call withContext builder function to switch the context in the same coroutine.

val job = Job()val dispatcher: CoroutineDispatcher = Dispatchers.Defaultval exceptionHandler = CoroutineExceptionHandler { context, exception ->
// define what to do with caught exceptions
}
// actually this is an object of CombinedContext type
val coroutineContext : CoroutineContext = job + dispatcher + exceptionHandler
fun launchCoroutineInExplicitContext() = runBlocking {
launch(coroutineContext) {
// some work
withContext(Dispatchers.Main) {
// some UI related work
}
}
}

Scope

You could have noticed that GlobalScope.launch was written in some place before. That’s because coroutines work within a certain scope, which is their execution space and implementation of the structured hierarchy. Actions taken on a scope affect all coroutines within it. Thanks to this, the problem of manual management of the status of all coroutines is eliminated. For example, instead of manually canceling every coroutine just simply cancel them through their scope. GlobalScope is a scope referenced to the whole app lifecycle, so try to avoid it if possible. In order to define your own scope for any class just implement CoroutineScope and override coroutineContext.

class SomeClass {

private lateinit var scope: CoroutineScope

fun create() {
// can't just call launch, no scope provided
scope = CoroutineScope(Dispatchers.Main).launch {
// now it's possible to run a coroutine
}
}
fun destroy() {
scope.cancel()
}
}
class ScopeActivity : AppCompatActivity(), CoroutineScope {

override val coroutineContext: CoroutineContext
get() = Dispatchers.Main
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

// call launch is possible because
// Activity is CoroutineScope itself
launch {
}
}

override fun onDestroy() {
super.onDestroy()
cancel() // cancel the whole scope - all coroutines within
}
}
// provide CoroutineScope by delegate
class ScopeDelegateActivity : AppCompatActivity(), CoroutineScope by MainScope() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
launch { }
}
}

Fortunately, the androidx.lifecycle library provides extensions for some components that create coroutine scope for you, so there is no need to implement CoroutineScope for each Activity or ViewModel.

class LifecycleScopeActivity : AppCompatActivity() {    override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

lifecycleScope.launch {
// scope bounded to the lifecycle of Activity
}
}
}
class LifecycleScopeViewModel : ViewModel() { init {
viewModelScope.launch {
// scope bounded to the lifecycle of ViewModel
}
}
}

Cancellation

When cancel is called on a scope or a job, it doesn’t mean that it happens immediately. Moreover it could never happen — the code must be cancelable. If a coroutine has been canceled, a CancellationException exception is thrown and the operation aborts. However, if a block of code is not inside the suspend function (each one is cancellable), there is no auto check of the running state, which causes the code to not respond to cancel requests. To make cancellation possible, check if it has been canceled manually through isActive property or yield function. To set timeout execution use withTimeout and withTimeoutOrNull functions.

fun cancelOnlySuspend() = runBlocking {
val cancelable = launch {
repeat(10) {
suspendWork()
}
}
val notCancelable = launch {
repeat(10) {
notSuspendWork()
}
}
delay(25)
cancelable.cancel() // suspendWork called 3 times
notCancelable.cancel() // notSuspendWork called 10 times
}
fun checkCancelManually() = runBlocking {
val checkIsActive = launch {
repeat(10) {
// check periodically is the scope still active
// or has been cancelled

// or use ensureActive to throw CancellationException
if (isActive) {
notSuspendWork()
// complete this task even if
// cancel has been called during the work
}
}
}
val callYield = launch {
repeat(10) {
notSuspendWork()
yield() // periodically suspend the work
}
}
delay(25)
checkIsActive.cancel() // suspendWork called 3 times
callYield.cancel() // suspendWork called 3 times
}
fun cancelByTimeout() = runBlocking {
// throws TimeoutCancellationException
withTimeout(25) {
repeat(10) {
suspendWork()
}
}
// returns null instead of throwing an exception
withTimeoutOrNull(25) {
repeat(10) {
suspendWork()
}
}
// both blocks called 3 times
}
fun clearOnFinally() = runBlocking {
val cancelable = launch {
try {
repeat(10) {
suspendWork()
}
} finally {
// use only non suspending functions
withContext(NonCancellable) {
// here you can use suspending functions
}
}
}
delay(25)
cancelable.cancel() // throw a CancellationException
}
suspend fun suspendWork() {
delay(10)
}
fun notSuspendWork() {
Thread.sleep(10)
}

Exception

A single coroutine throws CancellationException to cancel itself and this is expected behaviour and we don’t have to worry about it. But what happens if another exception was thrown? How to handle it? It depends. Propagating exceptions can be done automatically (e.g. for launch) or by exposing them to users (e.g. async). The first thing we need to know is that CoroutineExceptionHandler, which is a part of scope, invokes only on uncaught exceptions. Coroutines created in the context of Job — by launch, delegate handling of their exceptions to their parent and so on until root coroutine. So CoroutineExceptionHandler is ignored for all children coroutines and makes sense only for root. Moreover, coroutines created by async always catch exceptions and attach them in the Deferred result so CoroutineExceptionHandler doesn’t apply either. When multiple children coroutines fail with an exception, only the first one is handled.

fun useExceptionHandler() = runBlocking {
val exceptionHandler = CoroutineExceptionHandler { _, exception ->
// some print, analytics, etc
}
val job = GlobalScope.launch(exceptionHandler) {
// GlobalScope so root coroutine
throw NullPointerException()
}
val deferred = GlobalScope.async(exceptionHandler) {
// root coroutine
throw IndexOutOfBoundsException()
}
job.join()
deferred.join()
// exceptionHandler only caught an exception from job
deferred.await()
// now the exception is thrown
// and not caught by exceptionHandler
}
fun tryCatchExceptions() = runBlocking {
val exceptionHandler = CoroutineExceptionHandler { _, exception ->
// some print, analytics, etc
}
val job = GlobalScope.launch(exceptionHandler) {
try {
throw NullPointerException()
} catch (e: Exception) {
// some cleaning work
}

}
val deferred = GlobalScope.async(exceptionHandler) {
try {
throw IndexOutOfBoundsException()
} catch (e: Exception) {
// some cleaning work
}
}
job.join()
deferred.await()
// exceptionHandler didn't catch any exception
// because no one was uncaught
}
fun handleExceptionWhenAllChildrenTerminate() = runBlocking {
val exceptionHandler = CoroutineExceptionHandler { _, exception ->
// some print, analytics, etc
}
val job = GlobalScope.launch(exceptionHandler) {
launch {
try {
delay(1000) // some time consuming work
} finally {
withContext(NonCancellable) {
// a coroutine is cancelling, do some work
delay(25)
// more work
}
}
}
launch {
delay(10)
throw NullPointerException()
}
}
job.join()
// exceptionHandler handled an exception after 25ms
// when all children are cancelled
}

Cancellation is bidirectional and propagates through the whole coroutine hierarchy. So one uncaught exception from any child cancels the whole root coroutine. If an opposite unidirectional relationship is required, SupervisorJob can be used as a part of coroutine scope, or just supervisorScope as a scope itself (instead of coroutineScope). For that case, every child should handle its exceptions on their own because any failure doesn’t propagate to the parent.

fun supervisorJob() = runBlocking {
val supervisorJob = SupervisorJob()
with(CoroutineScope(coroutineContext + supervisorJob)) {
val childA = launch {
delay(10)
throw NullPointerException()
}
val childB = launch {
delay(1000)
// some work
}
// childA cancelled by an exception
childA.join()
delay(25)
// childB is still working, not canceled by childA fail
supervisorJob.cancel()
// now childB is cancelled by parent
childB.join()
}
}
fun supervisorScope() = runBlocking {
val exceptionHandler = CoroutineExceptionHandler { _, exception ->
// some print, analytics, etc
}
supervisorScope {
val childWithOwnHandler = launch(exceptionHandler) {
// child has own exception handler
delay(10)
throw NullPointerException()
}
delay(25)
// some work here still possible
// only childWithOwnHandler was cancelled
}
}

Conclusion

In this blogpost I have attempted to unravel some of the mystery behind Kotlin Coroutines. I have demonstrated the what, where and how of coroutines and presented what benefits they bring to the table, the most important of which is undoubtedly performance. Coroutines, in fact, make it easy to write performant and thread-safe code and, because they are native to Kotlin, can be considered to be the best solution for asynchronous code, especially if you create a new app. However, this blogpost covers only just the surface of what Kotlin Coroutines have to offer and beyond is where really exciting stuff happens, so stay tuned for more to come.

Originally published at https://dev.fandom.com.

--

--