Chapters

Hide chapters

Kotlin Coroutines by Tutorials

Second Edition · Android 10 · Kotlin 1.3 · Android Studio 3.5

Section I: Introduction to Coroutines

Section 1: 9 chapters
Show chapters Hide chapters

7. Context Switch & Dispatching
Written by Filip Babić

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

Right about now, you’ve amassed a good amount of knowledge about coroutines, suspendable functions and the Kotlin’s Coroutines API. But you haven’t learned much about how you can deal with threading, and which threading solutions exist within the API itself. However, you did learn what the CoroutineContext is, and what it’s used for. Having the ability to combine multiple CoroutineContexts, and different context types, to produce powerful coroutine mechanisms makes coroutines really extensible and versatile.

In fact, the CoroutineContext is a fundamental part of something called context switching, and the process of dispatching, which in turn revolves around threading.

Work scheduling

Organizing work at a system level is the bread and butter of all things related to multi-threading and parallel computing. Back in the day, when systems had a single core processor and could only utilize a single thread, it was extremely important to write optimized organizing algorithms so that the system didn’t freeze up and so that actions didn’t take forever to complete.

The process of figuring out the order, severity and resource usage for units of work the system needs to complete is called scheduling. Just like with a regular schedule, which holds all your meetings and chores, it serves to best organize which event should happen before others. It also deals with where the work lives in memory and when it starts or ends — the lifecycle and how it behaves when things break.

So when the system receives, let’s say, five events it needs to process, it first looks at the computational power it has available. If it’s already under 70% load, then it cannot take on a task which would require 40% of the total load. It then tries to fill in the available computational power by dividing the resources between other tasks which won’t overload the system. But there’s a caveat here, if you keep trying to fill in the workload with smaller tasks, you may never get to free up enough computational power to finally process the bigger unit of work.

In an operating system, all of these responsibilities belong to a construct called a scheduler. Schedulers decide when and how they should assign the computer’s resources to which tasks. They also take care of the lifecycle of the work you give them, since the events won’t start until a scheduler gives the system a green light, nor will they finish until they are completely processed. If any of the events breaks, an exception occurs, the scheduler is notified, and the system kills the process.

In terms of coroutines and modern-day systems, scheduling usually comes down to the distribution and organization of work between threads in thread pools. They allow the system to abstract away all of the responsibilities in one seemingly simple object.

Swimming in a pool of threads

A thread pool is a number of threads pooled together and distributed between work events that the system receives in its queue. Today’s hardware supports doing multiple things at the same time and effectively handling quite a few times the amount of work than before due to multiple cores. Combining that with the fact that coroutines can be completed one piece at a time, instead of running the entire operation, it can make coroutines extremely performant. This allows you to run several coroutines at once and schedule threads in such a way that each of the threads does a bit of work on each of the coroutines until all of the work is done, while all of the threads are constantly being reassigned.

Context switching

In the Coroutines API, you don’t have to worry about creating your own threads or thread pools, or about scheduling how multiple coroutines are executed, and their lifecycle. The Coroutines API has a specific way of communicating all of this information — via ContinuationInterceptors, which you provide through Dispatchers, which you’ll learn about later in this chapter.

Explaining ContinuationInterceptors

Even though this chapter mentions ContinuationInterceptors, it may still be a bit unclear on how they work. If you remember from the diagram of what happens with functions in the call stack and when suspendable functions are called:

Call stack with Continuation
Lutk ssecq tipc Qanjimeegoun

abstract fun <T> interceptContinuation(
    continuation: Continuation<T>
): Continuation<T>

Coroutine dispatcher types

Kotlin provides a concise way of communicating threading options in coroutines using Dispatchers. They are a CoroutineContext.Element implementation, forming one part of the puzzle that handles how coroutines behave when executed. In general computing, a dispatcher is a module that gives control of the CPU to whichever process the scheduling mechanism selected for execution. So a scheduler decides which process is next in line for a bit of CPU power, but it passes the process down to a dispatcher to allow the process to use up the actual resources. Together, these two modules or mechanisms control processes in an operating system.

Default dispatcher

The default dispatcher’s name pretty much gives it away. It’s used in the foundation of coroutines and is used whenever you don’t specify a dispatcher. It’s convenient to use because it’s backed by a worker thread pool, and the number of tasks the Default dispatcher can process is always equal to the number of cores the system has, and is at least two. Because the entire threading mechanism and the thread pool is pre-built, you can rely on it for your day-to-day work related to coroutines and operations you want to off-load from the main thread.

IO dispatcher

Again, the name says a lot. Whenever you’re trying to process something with input and output, like uploading or decrypting/encrypting files, you can use this dispatcher to make things easier for you. That being said, it’s bound to the JVM, so if you’re using Kotlin/JavaScript or Kotlin/Native projects, you won’t be able to use it.

Main dispatcher

This dispatcher is tied to systems that have some form of user interface, such as Android or visual Java applications. And as mentioned, it dispatches work to the thread that handles UI objects. You cannot use this without a UI, and if you try to call Dispatchers.Main in a project that doesn’t use Swing, JavaFX or isn’t an Android app, your code will crash.

Using dispatchers

Now that you know which dispatchers are out there, it’s time to learn how to utilize them. Import this chapter’s starter project, using IntelliJ, and selecting Import Project, and navigating to the context-switch-and-dispatching/projects/starter folder, selecting the context-switch-and-dispatching project. Let’s say you had the following code:

GlobalScope.launch {
  println("This is a coroutine")
}
public fun CoroutineScope.launch(
  context: CoroutineContext = EmptyCoroutineContext,
  start: CoroutineStart = CoroutineStart.DEFAULT,
  block: suspend CoroutineScope.() -> Unit
): Job
fun main() {
  GlobalScope.launch { println(Thread.currentThread().name) }

  Thread.sleep(50)
}
DefaultDispatcher-worker-1
fun main() {
  GlobalScope.launch(context = Dispatchers.Default) { 
    println(Thread.currentThread().name) 
  }

  Thread.sleep(50)
}
fun main() {
  GlobalScope.launch(context = Dispatchers.Unconfined) { 
    println(Thread.currentThread().name) 
  }

  Thread.sleep(50)
}

Creating a work stealing Executor

With the standard Coroutines API, you also have the ability to create new threads or thread pools for coroutines. This is done by creating a new Executor. Executors are objects that execute given tasks. They are usually tied with Runnables, since they wrap the task in a runnable, which needs executing. Creating a work-stealing executors for example means that it will use all available resources at its disposal, in order to achieve a certain level of parallelism, which you can define. To use the work-stealing-executor, you have to do the following:

fun main() {
  val executorDispatcher = Executors
      .newWorkStealingPool()
      .asCoroutineDispatcher()

  GlobalScope.launch(context = executorDispatcher) {
    println(Thread.currentThread().name)
  }

  Thread.sleep(50)
}
ForkJoinPool-1-worker-9

Key points

  • One of the most important concepts in computing, when using execution algorithms, is scheduling and context switching.
  • Scheduling takes care of resource management by coordinating threading and the lifecycle of processes.
  • To communicate thread and process states in computing and task execution, the system uses context switching and dispatching.
  • Context switching helps the system store thread and process state, so that it can switch between tasks which need execution.
  • Dispatching handles which tasks get resources at which point in time.
  • ContinuationInterceptors, which take care of the input/output of threading, and the main and background threads are provided through the Dispatchers class.
  • Dispatchers can be confined and unconfined, where being confined or not relates to using a fixed threading system.
  • There are four main dispatchers: Default, IO, Main and Unconfined.
  • Using the Executors class you can create new thread pools to use for your coroutine work.
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 accessing parts of this content for free, with some sections shown as scrambled text. Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now