Chapters

Hide chapters

Kotlin Apprentice

Second Edition · Android 10 · Kotlin 1.3 · IDEA

Before You Begin

Section 0: 3 chapters
Show chapters Hide chapters

Section III: Building Your Own Types

Section 3: 8 chapters
Show chapters Hide chapters

Section IV: Intermediate Topics

Section 4: 9 chapters
Show chapters Hide chapters

23. Kotlin Coroutines
Written by Irina Galata

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

While working through the previous chapters, you’ve run synchronous code only. That means that one command was executed after another by your CPU, sequentially, and no code in your projects were running simultaneously on different computing cores (in the case that your CPU has them, which they tend to these days).

Consequently, if you decided to perform any long-running, time-consuming operations (e.g., sending a request over a network to a server, or processing a large file), your program would appear to freeze until the operation finished, and a user would have to wait. That’s less than ideal — a user should be able to interact with your program even while it’s executing a difficult task. That expectation leads to the concept of the asynchronous programming.

Asynchronous programming

As opposed to the synchronous approach, asynchronous programming allows for the execution of several tasks in parallel at the same time. That way, you can render a beautiful loader animation while your app is also retrieving the necessary data from a server, for example. Or you could break up a non-trivial task into a few easier ones and execute them simultaneously to decrease the processing time.

Threads

In Java — and accordingly in Kotlin on the JVM — you can parallelize your program using threads. Each java.lang.Thread object represents one execution flow, which sequentially performs the commands within the single thread.

fun main() {
  thread(start = true, name = "another thread") {
    (0..10).forEach {
      println("Message #$it from the ${Thread.currentThread().name}")
    }
  }

  (0..10).forEach {
    println("Message #$it from the ${Thread.currentThread().name}")
  }
}

Coroutines

There isn’t an immediate better option in the Java language but, in Kotlin, you receive coroutines right out of the box! A coroutine is primarily a computation. Its defining feature is that it can be suspended and resumed at specified points of the computation without blocking a thread. Suspension is an extremely efficient operation. You can create hundreds and even thousands of coroutines and run them concurrently, as they are lightweight and don’t require many extra resources for their execution.

Getting started

Open up the starter project for this chapter. The starter project contains a non-coroutine version of the example project we’ll build below using coroutines. The main() function in main.kt looks as follows:

fun main() {
  BuildingYard.startProject("Smart house", 20)
}
dependencies {
    ...
    compile "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.0"
}
fun main() = runBlocking {
  launch(Dispatchers.Default) {
    (0..10).forEach {
      println("Message #$it from the ${Thread.currentThread().name}")
    }
  }

  (0..10).forEach {
    println("Message #$it from the ${Thread.currentThread().name}")
  }
}

Configuring coroutines

Kotlin coroutines are an extremely flexible solution for the wide variety of cases you may have. And the way a coroutine behaves is pretty much defined by its context. Any coroutine gets executed inside some CoroutineScope containing an instance of CoroutineContext, which is represented by a collection containing important configurations. You’re going to get acquainted with the most important of them - Job, Dispatcher and, later in this chapter, CoroutineExceptionHandler.

Job

Job basically represents a background job, which has a state (active, cancelled, completed, etc.), optionally has children, and can be started and cancelled. You’ll learn more about Job in this chapter.

Dispatchers

Dispatchers are responsible for the threads where your coroutines are executed. There are some ready-to-use dispatchers in the Kotlin core library:

CoroutineScope

CoroutineScope is an interface which does nothing except provide an associated CoroutineContext:

public interface CoroutineScope {
  public actual val coroutineContext: CoroutineContext
}

Obtaining a scope

There are multiple ways to get CoroutineScope to launch a coroutine. Some of them are mentioned here:

Coroutines builders

In order to use coroutines and therefore parallelize the execution of your code, you need to use coroutine builders. They’re regular functions that create a new coroutine inside a specified CoroutineContext. You’ve already seen some of them in the code snippet above — runBlocking() and launch(). Let’s find out how they work.

runBlocking()

The declaration of the runBlocking() function in the coroutine library code is as follows:

public fun <T> runBlocking(context: CoroutineContext
    = EmptyCoroutineContext, block: suspend CoroutineScope.() -> T): T

launch()

Our example code also used the launch() function, which has the following signature:

public fun CoroutineScope.launch(
  context: CoroutineContext = EmptyCoroutineContext,
  start: CoroutineStart = CoroutineStart.DEFAULT,
  block: suspend CoroutineScope.() -> Unit
): Job
public suspend fun join()
launch { postVideoToFeed() }.join()

CoroutineStart

As you can see from launch function declaration, you can specify not only the threads on which your coroutine will be launched, but also the moment when it should happen. There are four options:

async()

There will be numerous cases where you are interested not only in waiting for the coroutine to be executed, but also in getting a result from it. The most common case is getting data from a serverf—for example, loading a user profile or getting a list of chat messages. async() is a definite solution for this case:

public fun <T> CoroutineScope.async(
  context: CoroutineContext = EmptyCoroutineContext,
  start: CoroutineStart = CoroutineStart.DEFAULT,
  block: suspend CoroutineScope.() -> T
): Deferred<T>
public suspend fun await(): T
val userData = async { getUserDataFromServer() }.await()

withContext()

The withContext() function gets the result of the execution as well. However, it’s optimized for more straightforward cases, when you don’t need the Deferred instance but just the result itself:

public suspend fun <T> withContext(
  context: CoroutineContext,
  block: suspend CoroutineScope.() -> T
): T

Example: A high-rise building

To illustrate all the niceties of coroutines, it’s necessary to imagine a process or task, some parts of which could be executed simultaneously, while other parts should be completed strictly one after another. The process of constructing a high-rise building is a good example.

class Building(val name: String) {

  fun makeFoundation() {
    Thread.sleep(300)
    speakThroughBullhorn("The foundation is ready")
  }

  fun buildFloor(floor: Int) {
    Thread.sleep(100)
    speakThroughBullhorn("The $floor'th floor is raised")
  }

  fun placeWindows(floor: Int) {
    Thread.sleep(100)
    speakThroughBullhorn("Windows are placed on the $floor'th floor")
  }

  fun installDoors(floor: Int) {
    Thread.sleep(100)
    speakThroughBullhorn("Doors are installed on the $floor'th floor")
  }

  fun provideElectricity(floor: Int) {
    Thread.sleep(100)
    speakThroughBullhorn("Electricity is provided on the $floor'th floor")
  }

  fun buildRoof() {
    Thread.sleep(200)
    speakThroughBullhorn("The roof is ready")
  }

  fun fitOut(floor: Int) {
    Thread.sleep(200)
    speakThroughBullhorn("The $floor'th floor is furnished")  
  }

  fun speakThroughBullhorn(message: String) = println(message)

}
class Building(val name: String, var floors: Int = 0, private val scope: CoroutineScope) {

  suspend fun makeFoundation() = scope.launch {
    delay(300)
    speakThroughBullhorn("[${Thread.currentThread().name}] The foundation is ready")
  }

  suspend fun buildFloor(floor: Int) = scope.launch {
    delay(100)
    speakThroughBullhorn("[${Thread.currentThread().name}] Floor number $floor floor is built")
    ++floors
  }

  suspend fun placeWindows(floor: Int) = scope.launch {
    delay(100)
    speakThroughBullhorn("[${Thread.currentThread().name}] Windows are placed on floor number $floor")
  }

  suspend fun installDoors(floor: Int) = scope.launch {
    delay(100)
    speakThroughBullhorn("[${Thread.currentThread().name}] Doors are installed on floor number $floor")
  }

  suspend fun provideElectricity(floor: Int) = scope.launch {
    delay(100)
    speakThroughBullhorn("[${Thread.currentThread().name}] Electricity is provided on floor number $floor")
  }

  suspend fun buildRoof() = scope.launch {
    delay(200)
    speakThroughBullhorn("[${Thread.currentThread().name}] The roof is ready")
  }

  suspend fun fitOut(floor: Int) = scope.launch {
    delay(200)
    speakThroughBullhorn("[${Thread.currentThread().name}] Floor number $floor is furnished")
  }

  fun speakThroughBullhorn(message: String) = println(message)
}
class BuildingYard {
  suspend fun startProject(name: String, floors: Int) {

  }
}
fun main() = runBlocking {
  BuildingYard().startProject("Smart house", 20)
}
suspend fun startProject(name: String, floors: Int) {
  val building = withContext(Dispatchers.Default) {
    val building = Building(name, scope = this)
    val cores = Runtime.getRuntime().availableProcessors()
    building.speakThroughBullhorn(
      "The building of $name is started with $cores building machines engaged")
    building.makeFoundation().join()
    building
  }
  if (building.floors == floors) {
    building.speakThroughBullhorn("${building.name} is ready!")
  }
}

suspend fun startProject(name: String, floors: Int) {
  val building = withContext(Dispatchers.Default) {
    val building = Building(name, scope = this)

    val cores = Runtime.getRuntime().availableProcessors()

    building.speakThroughBullhorn("The building of $name is started with $cores building machines engaged")
    // Any other phases couldn't be started until foundation isn't ready
    building.makeFoundation().join()

    (1..floors).forEach {
      // A floor should be raised before we can decorate it
      building.buildFloor(it).join()

      // These decorations could be made at the same time
      building.placeWindows(it)
      building.installDoors(it)
      building.provideElectricity(it)
      building.fitOut(it)
    }

    building.buildRoof().join()
    building
  }

  if (building.floors == floors) {
    building.speakThroughBullhorn("${building.name} is ready!")
  }
}

Error handling

The common approach to handle exceptions while using coroutines is a well-known try-catch block. The way you catch exceptions in synchronous code is still applicable here:

try {
  val userProfile = scope.withContext(Dispatchers.IO) { getProfile() }
} catch (e: NoSuchUserException) {
  // handle exception
}

Using CoroutineExceptionHandler

There could be a case when you need to have a global exception handler for all your coroutines, and CoroutineExceptionHandler is designed for this purpose:

val scope = CoroutineScope(Dispatchers.Default)
val handler = CoroutineExceptionHandler { context, exception ->
  println(exception.message)
}
scope.launch(handler) {
  uploadData()
}

Understanding coroutines

Coroutines aren’t a new concept in software development; several programming languages — such as C#, Ruby and Python — have supported them for a long time. In many languages, coroutines are based on state machines, and Kotlin isn’t an exception.

Challenges

Challenge 1

Modify the BuildingYard class in such way that you could build several buildings simultaneously, not one by one. (Hint: Consider using Collection<Deferred<T>>.awaitAll())

Challenge 2

Modify the Building class in such way so the buildFloor() function could fail randomly (i.e., throw an exception). In the BuildingYard class, after this function execution completes, check whether it executed successfully. If it is unsuccessful, start the execution of the task again.

Key points

  • The asynchronous approach to programming focuses on allowing you to execute several operations at the same time.
  • Threads are used when you don’t need a lot of them to perform the necessary tasks.
  • Coroutines are like “lightweight threads”, since they don’t require as much memory resources and they’re not based on OS level threads like Java threads.
  • A large number of coroutines could be executed on a single thread without blocking it.
  • Each coroutine is bound to some CoroutineContext.
  • CoroutineContext is responsible for many important parts of a coroutine such as its Job, Dispatcher and CoroutineExceptionHandler.
  • Use coroutines builders (runBlocking(), withContext(), launch(), async()) to create and launch coroutines.
  • You can decide when to launch your coroutine using CoroutineStart.
  • Use dispatchers to define the threads for your coroutine execution.
  • Coroutines are based on the concept of a state machine, with each state referring to a suspension point. It doesn’t require extra time or resources to switch between coroutines and to restore their state.

Where to go from here?

There are several ways you could parallelize your code execution in Kotlin. One example is the reactive approach, which is becoming quite popular. ReactiveX or Rx is an API for asynchronous programming implemented by a wide variety of platforms and programming languages (e.g., Kotlin, Java, Swift, Python, etc.).

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.

Unlock now