Kotlin Coroutines (2/3)

Continue our Coroutines talk. Let's discuss Coroutines Context.

2023-02-13 13:08:46 - Mohamad Abuzaid

If you haven’t already, It is recommended to read the previous article first:

In this second article we will cover a single but very important topic in Kotlin Coroutines:

---------------------

[4] Coroutine Context


The coroutine context is a set of elements that provide the environment in which a coroutine runs. It includes information about the coroutine’s job, its dispatcher, and any additional elements that are needed for the coroutine to run correctly.

A coroutine context is represented by the CoroutineContext interface in Kotlin. It is a combination of elements that are stored as a set of key-value pairs. The most commonly used elements in the coroutine context are the job and the dispatcher.

fun main() {
    runBlocking {
        launch(Dispatchers.Default) {
            println("Coroutine Context: $coroutineContext")
            println("Job: ${coroutineContext[Job]}")
            println("Dispatcher: ${coroutineContext[CoroutineDispatcher]}")
        }
    }
}

Here, we launch a coroutine with a dispatcher of Dispatchers.Default. We then use the coroutineContext property to access the coroutine context, and the properties Job and CoroutineDispatcher to access the job and dispatcher elements respectively.

The coroutine context can also be used to add additional elements to the context using the plus operator. For example:

fun main() {
    runBlocking {
        val customElement = "Custom Element"
        launch(Dispatchers.Default + CoroutineName("MyCoroutine")) {
            println("Coroutine Context: $coroutineContext")
            println("Job: ${coroutineContext[Job]}")
            println("Dispatcher: ${coroutineContext[CoroutineDispatcher]}")
            println("Custom Element: ${coroutineContext[customElement]}")
        }
    }
}

In this example, we add a custom element to the context by using the plus operator to combine the Dispatchers.Default dispatcher with a CoroutineName element. The custom element can then be accessed using the key customElement.

CoroutineContext has some elements:

  1. CoroutineDispatcher: Represents the thread pool or the specific thread on which the coroutine is running.
  2. Job: Represents the lifecycle of the coroutine and provides information such as whether it is active, completed, or cancelled.
  3. CoroutineName: Provides a way to give a descriptive name to a coroutine.
  4. Element: Represents a custom element that can be added to the coroutine context to provide additional information or functionality.
  5. CoroutineExceptionHandler: Represents the mechanism to handle exceptions thrown in the coroutine.

-----------------

(a) Coroutine Dispatchers


Coroutine Dispatchers are responsible for executing coroutines. They determine which thread or threads a coroutine runs on and how it should be scheduled.

In Kotlin, there are several built-in dispatchers that you can use, including:

fun main() = runBlocking {
    launch(Dispatchers.Main) {
        // this block of code will run on the main thread
        updateUIView()
    }
}


fun main() = runBlocking {
    launch(Dispatchers.Default) {
        // this block of code will run in the background using the Default dispatcher
        println("Hello from Default")
    }
}


fun main() = runBlocking {
    launch(Dispatchers.IO) {
        // this block of code will run in the background using the IO dispatcher
        fetchDataFromBackend()
    }
}


fun main() = runBlocking {
    launch(Dispatchers.Unconfined) {
        // this block of code will run on the calling thread, and if the calling thread is blocked, it will switch to another thread
        println("Hello from Unconfined")
    }
}

In each of these examples, the launch function is used to create a coroutine that runs on the specified dispatcher. The runBlocking function is used to create a coroutine that blocks the current thread, which is useful for synchronizing the execution of multiple coroutines.

-------------

(b) Coroutine Jobs


A Job in coroutines is an abstract representation of the lifecycle of a coroutine. It provides information about the state of the coroutine, such as whether it is active, completed, or cancelled.

fun main() {
    runBlocking {
        val job = launch {
            // Coroutine code
        }

        // Wait for the job to complete
        job.join()

        // Check if the job is active
        println("Job is active: ${job.isActive}")

        // Check if the job has completed
        println("Job has completed: ${job.isCompleted}")

        // Cancel the job
        job.cancel()

        // Check if the job has been cancelled
        println("Job has been cancelled: ${job.isCancelled}")
    }
}

In this example, a coroutine job is created using the launch builder. The join function is used to wait for the job to complete. The isActive, isCompleted, and isCancelled properties can be used to check the state of the job. The cancel function can be used to cancel the job.

Jobs can also be organized in a parent-child relationship, where a parent job is responsible for the lifecycle of its child jobs. When a parent job is cancelled, all of its child jobs are also cancelled.

fun main() {
    runBlocking {
        val parentJob = Job()
        val scope = CoroutineScope(coroutineContext + parentJob)

        val job1 = scope.launch {
            // Coroutine code
        }

        val job2 = scope.launch {
            // Coroutine code
        }

        // Cancel the parent job
        parentJob.cancel()

        // Check if the child jobs have been cancelled
        println("Job 1 has been cancelled: ${job1.isCancelled}")
        println("Job 2 has been cancelled: ${job2.isCancelled}")
    }
}

In this example, a parent job is created using the Job class, and a coroutine scope is created using CoroutineScope and the parent job. Two child jobs are created using the launch builder in the scope of the parent job. When the parent job is cancelled, both child jobs are also cancelled.


- Job Join:

The join function in coroutines allows you to wait for a coroutine job to complete before continuing with the rest of your code. This is useful when you need to make sure that the results of a coroutine are available before using them.

fun main() {
    runBlocking {
        val job1 = launch {
            // Coroutine code
        }

        // Wait for the job to complete
        job1.join()

        val job2 = launch {
            // Coroutine code
        }
    }
}

In this example, a coroutine job is created using the launch builder. The join function is used to wait for job1 to complete. Once the job1 has completed, the rest of the code is executed. i.e. job2 will not start until job1 is completed.

It’s important to note that the join function blocks the current thread until the job has completed. This means that other coroutines that are running in the same context may not make progress while the join function is waiting. To avoid this, you can use the await function in the async builder to wait for a coroutine to complete.


- Job Cancel:

The cancel function in coroutines allows you to cancel a coroutine job. Cancelling a job means that the coroutine is stopped and any further work that it would have done is not performed.

fun main() {
    runBlocking {
        val job = launch {
            for (i in 1..10) {
                println("Job running: $i")
                delay(100)
            }
        }

        delay(500)

        // Cancel the job
        job.cancel()

        // Wait for the job to complete (or be cancelled)
        job.join()

        // Continue with the rest of the code
        println("Job has completed.")
    }
}

Here, a coroutine job is created using the launch builder. After a delay of 500 milliseconds, the job is cancelled using the cancel function. The join function is used to wait for the job to complete (or be cancelled). Once the job has completed, the rest of the code is executed, and the message “Job has completed.” is printed to the console.

It’s important to note that just cancelling a coroutine job does not guarantee that it will stop immediately. The coroutine can continue executing until it reaches a point where it checks if it has been cancelled. At that point, it will stop executing. To ensure that the coroutine stops as soon as possible, you can use the isActive property in the coroutine to check if it has been cancelled. If the coroutine has been cancelled, it can immediately stop executing.


- Supervisor Job:

A SupervisorJob is a type of coroutine job that acts as a supervisor for other coroutine jobs. When a coroutine job is created as a child of a SupervisorJob, it becomes a supervised job, and its lifecycle is tied to the lifecycle of its supervisor job.

A SupervisorJob can be created using the SupervisorJob constructor or the supervisorJob function in the kotlinx.coroutines library.

fun main() {
    runBlocking {
        val supervisor = SupervisorJob()

        // Create a coroutine scope with the supervisor job
        with(CoroutineScope(coroutineContext + supervisor)) {
            launch {
                println("Child 1 started")
                delay(1000)
                println("Child 1 finished")
            }

            launch {
                println("Child 2 started")
                delay(1000)
                println("Child 2 finished")
            }
        }

        // Wait for all children to complete
        supervisor.join()
    }
}

In this example, a SupervisorJob is created using the SupervisorJob constructor. Two child coroutines are then created using the launch builder, and they are launched within a coroutine scope that is created using the with function. The coroutine scope is created with the supervisor job as its context, making the two child coroutines supervised jobs.

When a SupervisorJob is cancelled, all of its supervised coroutines are cancelled as well. However, if a supervised coroutine fails due to an exception, it will not cancel the other supervised coroutines. Instead, the failed coroutine will be the only one that stops executing, and the others will continue running.

---------------

(c) CoroutineName


CoroutineName is an element in the coroutine context that provides a way to give a descriptive name to a coroutine. The name can be used for debugging and logging purposes, and can make it easier to understand the relationships between coroutines in your code.

fun main() {
    runBlocking {
        launch(Dispatchers.Default + CoroutineName("MyCoroutine")) {
            println("Coroutine Context: $coroutineContext")
            println("Job: ${coroutineContext[Job]}")
            println("Dispatcher: ${coroutineContext[CoroutineDispatcher]}")
            println("Name: ${coroutineContext[CoroutineName]}")
        }
    }
}

Here, we create a coroutine with a dispatcher of Dispatchers.Default, and add a CoroutineName element to the coroutine context using the plus operator. The coroutine name can then be accessed using the key CoroutineName in the coroutine context.

It’s important to note that while it’s possible to access the coroutine name in the coroutine context, it is not guaranteed to be unique, as multiple coroutines can have the same name. However, having unique names for coroutines can still be useful for debugging and logging purposes, as it makes it easier to identify which coroutine is executing a particular piece of code.

---------------

(d) Coroutine Element


In Coroutines, you can define a custom element that can be added to the coroutine context to provide additional information or functionality.

class CustomElement(val value: String) : CoroutineContext.Element {
    companion object Key : CoroutineContext.Key<CustomElement>
}

fun main() {
    runBlocking {
        val customElement = CustomElement("MyValue")

        launch(coroutineContext + customElement) {
            println("Custom Element: ${coroutineContext[CustomElement]}")
        }
    }
}

-----------------


(e) Coroutine Exception Handler


Exceptions can occur in coroutines just like they can occur in regular code. To handle exceptions in coroutines, you can use the try and catch keywords, similar to how you would in regular code.

fun main() {
    runBlocking {
        val job = launch {
            try {
                println("Coroutine started")
                delay(1000)
                throw IllegalStateException("Exception occurred")
            } catch (e: IllegalStateException) {
                println("Exception caught: $e")
            } finally {
                println("Finally block executed")
            }
        }
        job.join()
    }
}

Here, a coroutine is launched using the launch builder. Within the coroutine, a try block is used to execute some code. If an exception is thrown in the try block, it will be caught in the catch block, where the exception can be handled. The finally block will be executed whether or not an exception is thrown.

In this example, after a 1 second delay, an IllegalStateException is thrown in the try block. This exception is caught in the catch block and printed to the console. Then, the finally block is executed.

It’s important to note that exceptions in coroutines are propagated up the coroutine hierarchy to the parent coroutine. If an exception is not caught in the current coroutine, it will be propagated to the parent coroutine, where it can be caught and handled. If an exception is not caught anywhere in the coroutine hierarchy, it will cause the coroutine to be cancelled, and the exception will be propagated to the coroutine context’s CoroutineExceptionHandler.


- CoroutineExceptionHandler:

CoroutineExceptionHandler is a type of exception handler in the Kotlin coroutines library that can be used to handle exceptions that occur in coroutines. When an exception is not caught in a coroutine, it will be propagated to the parent coroutine, and eventually to the CoroutineExceptionHandler if one is present in the coroutine context.

fun main() {
    val handler = CoroutineExceptionHandler { _, exception ->
        println("Exception handled: $exception")
    }
    runBlocking {
        val job = GlobalScope.launch(handler) {
            throw IllegalStateException("Exception occurred")
        }
        job.join()
    }
}

In this example, a CoroutineExceptionHandler is created and passed to the launch builder as the context argument. This handler will be used to handle any exceptions that are not caught in the coroutine.

In the coroutine, an IllegalStateException is thrown. This exception is not caught in the coroutine, so it will be propagated to the CoroutineExceptionHandler, which will print the exception to the console.

It’s important to note that the CoroutineExceptionHandler will handle exceptions for all coroutines that are launched using the same context and that it will handle all uncaught exceptions, even if they occur in different coroutines. To handle exceptions in a more fine-grained manner, you can use the try and catch keywords as described in the previous answer.


That's it for now… We will continue our Coroutines talk in the following and final article.

-----------------

Next Part ==> Kotlin Coroutines (part 3)

More Posts