Blog Infos
Author
Published
Topics
, , , ,
Published

Hey there! Welcome to my blog, where we’ll be exploring the fascinating world of coroutines in Kotlin. Today, we’re focusing on two important concepts: CoroutineScope and CoroutineContext. Our main goal is to take a deep dive into CoroutineContext and understand its role in Kotlin coroutines.

Throughout this blog, we’ll break down elements like Job, CoroutineDispatcher, CoroutineName, and CoroutineExceptionHandler. By doing so, we aim to give you a clear understanding of CoroutineContext and how it impacts Kotlin development.

Join us as we uncover the ins and outs of CoroutineContext, providing practical insights into its application in Kotlin programming.

Coroutine Scope

A CoroutineScope keeps track of any coroutine it creates using launch or async. We are already familiar with CoroutineScope as all coroutine builders we have used in our previous blogs are an extension on it.

In Android, some KTX libraries provide their own CoroutineScope for certain lifecycle classes. For example, ViewModel has a viewModelScope, and Lifecycle has lifecycleScope. Unlike a dispatcher, however, a CoroutineScope doesn’t run the coroutines.

Coroutine Context

CoroutineContext contains a set of elements and using those elements it defines the behaviour of a coroutine.

A CoroutineContext consists of following elements:

  1. Job : Controls the lifecycle of a coroutine
  2. CoroutineDispatcher : Dispatches the work to a appropriate thread
  3. CoroutineName : Defines the name of the coroutine
  4. CoroutineExceptionHandler : Handles the uncaught exceptions (will go in detail on this in future blogs)

All coroutine builders like launch and async and custom scopes created using CoroutineScope() accept an optional CoroutineContext parameter. We can use this parameter to customise our coroutines.

Let’s discuss CoroutineContext elements in detail now

Job

Job is a handle to a coroutine that uniquely identifies a coroutine and manages it’s lifecycle. For every coroutine that is created within a scope, a new job instance is assigned and the other CoroutineContext elements are inherited from the containing scope.

CoroutineDispatcher

A coroutine dispatcher determines which thread or threads the coroutine will use for it’s execution. It determines the context in which the coroutine code runs.

Common Dispatcher’s include:

Dispatchers.Default
  • If no dispatcher is explicitly specified for a coroutine, Default dispatcher will be used
  • It used a shared pool of threads on JVM
  • Number of parallel tasks that can be performed by this Dispatcher is equal to the number of CPU cores on the system, but is at-least two.

Let’s see this with the help of an example:

fun defaultDispatcher() = runBlocking {
    for (i in 1..1000) {
        launch(Dispatchers.Default) {
            delay(50)
            println("I'm Dispatcher $i, working in ${Thread.currentThread().name}")
        }
    }
}

Here we have created 1000 coroutines with the default dispatcher and we are printing the thread name in which each of them is executed.

When I ran this code, only 10 different threads were used because the number of cores in my system is 10.

Note: You can also check your system’s number of cores by following below steps:

Click “About this mac” -> “System Report” -> “Hardware” The number of CPU Cores should be listed in the Hardware overview, to the right of “Total number of Cores”

Dispatchers.Main
  • This dispatcher is confined to the main thread operating on UI objects
  • In Android projects, this will be Android main thread
  • This should be used only for interacting with UI and quick work

If we run the previous example with Dispatchers.Main, it will give us an error:

Module with the Main dispatcher had failed to initialise.

Why did this happen?

This is because we are trying to use Main dispatcher in a plain JVM application. In order to work with the Main Dispatcher, we will have to setup our application.

For an android project, the Main dispatcher is provided by “kotlinx-coroutines-android” module.

Dispatchers.IO

Dispatchers.IO is optimized to handle blocking operations like disk/network I/O outside of Main thread. It follows a key concepts. Let’s understand each of them

Elasticity
  • Dispatchers.IO has a elastic pool thread. This means it can scale the number of threads up and down depending on the workload
  • The number of threads used by this dispatcher is limited by the value of “kotlinx.coroutines.io.parallelism” system property
  • The default value of parallelism is 64 threads or the number of cores, whichever is larger
  • The limitedParallelism fun returns a LimitedDispatcher object which is a view of Dispatchers.IO with specific parallelism limits.
  • Conceptually, there is an underlying dispatcher backed by an unlimited pool of threads, and both Dispatchers.IO and it’s views (limitedParallelism instances) share this pool
Thread Sharing
  • During peak loads, the system can use more threads, but in a steady state, only a small number of threads are active.
  • Dispatchers.IO also shares threads with Dispatchers.Default, which is use for CPU intensive tasks. So switching contexts from Dispatchers.Default to Dispatchers.IO might not lead to an actual thread switch.

Let’s see some practical examples now:

Let’s say we create two custom dispatchers for different resources with specific parallelism limits.

// 100 threads for MySQL connection
val myMysqlDbDispatcher = Dispatchers.IO.limitedParallelism(100)
// 60 threads for MongoDB connection
val myMongoDbDispatcher = Dispatchers.IO.limitedParallelism(60)

Despite setting these limits, the overall system thread usage can exceed the SUM of these limits during peak times. During peak times, the system can handle upto 224 threads (64 default + 100 for mySql + 60 for mongoDb). But in a steady state, the number of active threads is minimal.

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

,

Practical Patterns for Kotlin Coroutines in Production

Unlock the full potential of Kotlin Coroutines with a focused exploration of their practical applications in real-world scenarios. This talk will guide you through essential best practices, demonstrate robust patterns for common asynchronous tasks, and…
Watch Video

Practical Patterns for Kotlin Coroutines in Production

Marcin Moskała
Developer during the day, author at night, trainer
Kt. Academy

Practical Patterns for Kotlin Coroutines in Production

Marcin Moskała
Developer during the ...
Kt. Academy

Practical Patterns for Kotlin Coroutines in Production

Marcin Moskała
Developer during the day, ...
Kt. Academy

Jobs

Dispatchers.Unconfined
  • Dispatchers.Unconfined is a unique dispatcher which is not bound to a specific thread.
  • Coroutines that use unconfined dispatcher starts their initial execution on the thread that called it.
  • After the first suspend function in the coroutine, it resumes the coroutine in the thread that is determined by the suspending function that was invoked.
  • This dispatcher is appropriate for coroutines which neither consume CPU time nor update any shared data confined to a particular thread.

Let’s checkout a few examples using the unconfined dispatcher:

Example 1

This example explains how coroutines pick threads for a IO dispatcher and an unconfined dispatcher

fun confinedAndUnconfinedDispatcher() = runBlocking {
    launch (Dispatchers.IO){
        println("IO dispatcher, working in ${Thread.currentThread().name}")
        delay(1000)
        println("IO dispatcher, after delay I'm, working in ${Thread.currentThread().name}")
    }
    launch (Dispatchers.Unconfined){
        println("Unconfined dispatcher, working in ${Thread.currentThread().name}")
        delay(1000)
        println("Unconfined dispatcher, after delay I'm, working in ${Thread.currentThread().name}")
    }
}

When we run this code, we will see that IO dispatcher remains on the same thread even after the suspend function but that is not the case with the unconfined dispatcher.

Example 2

Let’s create nested coroutines, both using the Unconfined dispatcher.

fun nestedUnconfinedDispatcher() = runBlocking {
    launch(Dispatchers.Unconfined) {
        println("I'm in first launch before delay, working in ${Thread.currentThread().name}")
        delay(1000)
        println("I'm in first launch after delay, working in ${Thread.currentThread().name}")
        launch(Dispatchers.Unconfined) {
            println("I'm in second launch before delay, working in ${Thread.currentThread().name}")
            delay(1000)
            println("I'm in second launch after delay, working in ${Thread.currentThread().name}")
        }
    }
    println("I'm done, working in ${Thread.currentThread().name}")
}

When we run this code, we observe that the first coroutine begins execution on the main thread but switches to the default dispatcher after the delay function. Additionally, the nested coroutines operate entirely on the default dispatcher.

Example 3

In this example, we’ll use two different suspending functions to show how the coroutine can resume on different threads based on those functions’ implementations.

fun unconfinedWithContextSwitching() = runBlocking {
    println("Main program starts on thread: ${Thread.currentThread().name}")
    val job = launch(Dispatchers.Unconfined) {
        println("Coroutine starts on thread: ${Thread.currentThread().name}")
        delay(500)
        println("Coroutine resumes after delay on thread: ${Thread.currentThread().name}")
        withContext(Dispatchers.Default) {
            println("Inside withContext block on thread: ${Thread.currentThread().name}")
            delay(500)
        }
        println("Coroutine resumes after withContext on thread: ${Thread.currentThread().name}")
    }
    job.join()
    println("Main program ends on thread: ${Thread.currentThread().name}")
}

Let’s see the output first and then we will analyse it

Observations:

  • Coroutine starts on main thread
  • After the delay, it resumes in DefaultExecutor thread
  • After switching the context explicitly using withContext, coroutine resumes in the new worker thread even post the suspend function in it

There is one more interesting observation here:

The statement “Main program ends on thread…” actually prints at end but that wasn’t the case in our last example even though in both cases, it was after the launch block.

Why is it so??
  • This is because in this example we created a job object and called job.join here.
  • Without job.join, the runBlocking block doesn’t wait for the coroutine to finish, it immediately prints the final statement
  • With job.join, the runBlocking block waits for the coroutine to finish and prints the final statement after the launch coroutine finishes
  • This is because join is a suspend function and it suspends the parent coroutine until the job is completed for any reason
newSingleThreadContext
  • Creates a coroutine execution context using a single thread with built-in yield support
  • A dedicated thread is very expensive resource so in real applications it must be either released or stored in a top level variable and used throughout the application
launch(newSingleThreadContext("MyOwnThread")) { // will get its own new thread
    println("newSingleThreadContext: I'm working in thread ${Thread.currentThread().name}")
}

This is a delicate API and in most cases not needed in real world applications so we will not be going in much details of this.

CoroutineName
  • CoroutineName context element allows us to give a name to our coroutine.
  • This is useful in debugging coroutines (which will go in detail later)
  • The name gets included in the thread name that is executing this coroutine when the debugging mode is on
  • This can be combined with other context elements as well like dispatchers
launch(Dispatchers.Default + CoroutineName("MyCustomCoroutineName")) {
        println("I'm working in thread ${Thread.currentThread().name}")
    }

Note: We have enabled debug mode in a non-android application here by setting the “kotlinx.coroutines.debug” property. This will require you to add the following dependency to your project

implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-debug:$version'

That’s it for this article. Hope it was helpful! If you like it, please hit like.

This article is previously published on proandoiddev.com

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
It’s one of the common UX across apps to provide swipe to dismiss so…
READ MORE
blog
Hi, today I come to you with a quick tip on how to update…
READ MORE
blog
Automation is a key point of Software Testing once it make possible to reproduce…
READ MORE
blog
Drag and Drop reordering in Recyclerview can be achieved with ItemTouchHelper (checkout implementation reference).…
READ MORE
Menu