Coroutines in Kotlin
Amit Nadiger
Polyglot(Rust??, C++ 11,14,17,20, C, Kotlin, Java) Android TV, Cas, Blockchain, Polkadot, UTXO, Substrate, Wasm, Proxy-wasm,AndroidTV, Dvb, STB, Linux, Engineering management.
Co-Routines means cooperative?routines. i.e routines?which cooperate with each other .
It means when one routine?is executing , the other routine?will not interfere w.r.t to memory , cpu ,any resource by suspending itself i.e without blocking.
Coroutines are a lightweight and efficient concurrency mechanism that can be used in Kotlin to write asynchronous and non-blocking code. They provide a number of benefits compared to traditional threading models, including:
Coroutines are a powerful and flexible concurrency mechanism that can be used to write efficient, scalable, and resilient code in Kotlin. They are especially well-suited to modern, asynchronous programming models, such as those used in mobile and web applications, and can help developers to build applications that are more responsive, reliable, and maintainable.
Coroutines don't?have a dedicated stack . It means coroutine suspend execution by returning to the caller and the data that is required to resume execution is stored separately from the stack. Coroutines share the stack due to support for?suspension,.
How to convert the function in to coroutines . Its very easy in Kotlin - prepend the?suspend?keyword to the regular function as below :
suspend?fun backgroundTask(param: Int): Int {
// long running operation
}?
Under-hood conversion of Suspend by the compiler:?
Before we dive into the details, let's first understand the basics of coroutines. In Kotlin, a coroutine is a lightweight thread that can suspend and resume execution at specific points without blocking the thread. A coroutine can be thought of as a computation that can be paused and resumed at any point in time.
The main idea behind coroutines is to provide a way to write asynchronous, non-blocking code in a synchronous style. This makes the code more readable and easier to reason about. The suspend and resume mechanism is what makes this possible.
When a coroutine suspends, it means that it is pausing its execution and giving the control back to the caller. The coroutine can suspend for various reasons, such as waiting for an I/O operation to complete, waiting for a timer to expire, or waiting for another coroutine to complete its execution.
Please understand below about how function can be converted in to suspending function with introduction of Continuation<T>
fun backgroundTask(param: Int, callback:?Continuation<Int>): Int {
??// long running operation
}
a new?additional parameter of type?Continuation<T> is added as above .
Continuation<T>?is an interface that contains two functions that are invoked to resume the coroutine with a return value or with an exception if an error had occurred while the function was suspended.
Please see detailed article on this topic: (1) Under-hood trick of kotlin compiler on Coroutines | LinkedIn
How to create coroutines in Kotlin :
CoroutineBuilders help in creating coroutines.?Since?CoroutineBuilders?are not suspending themselves, they can be called from non-suspending code or any other piece of code. They act as a bridge between the suspending and non-suspending world.
Kotlin coroutine?builders (runBlocking , launch, async , withContext)
runBlocking:?blocks the current thread until all tasks of the coroutine it creates, complete.
Typically,?runBlocking?used to run tests on suspending functions. During tests, to make sure not to finish the test while we are doing heavy work in test suspend functions.
launch: “fire and forget” coroutine builder no return to caller.
1.launch creates a new coroutine that won’t return any result to the caller.
2.It also allows to start a coroutine in the background.
fun main() {
????GlobalScope.launch?{
?? println(doSomethingHeavy())
???? ----?do?something?-----?
?? }
?? runBlocking?{
? ? delay(3000L) // Keep JVM alive until coroutine is completed.
? }
}
suspend fun?doSomethingHeavy()?: String ? {
??delay(2000L) // simulate long running?heavy task
??return "Did some heavy?operation?that was 2 seconds long"
}
O/P : After 2 sec it?prints the string: "Did some heavy?operation?that was 2 seconds long".
async: coroutine builder which returns some value to the caller.
val userId = 1 // UserId
fun?main() {
??println(“get?userName?from Sever?")
??GlobalScope.launch?{
? ? ?val?userName = async {
? ? ? getUserNameFromServer(userId)
? ? ?}
? ? ?val??userAge?= async {
? ? ? getUserAgeFromServer(userId)
? ? }
? ? ?if (userName.await() &&?userAge.await()) {
? ? ? println(“UserName?of?userId($?userId?is: ${?userName?.await()} with Age =?${userAge.await()}?")
? ? ?} else {
? ? ? println("Please wait ,till both userName and userAge are fetched!") }
? ? ?}?
??}
??println("coroutine is waiting for a result...")
??runBlocking?{
?? delay(3000L) // only used to keep the JVM alive
??}
}
suspend fun??getUserNameFromServer??(Int:userId): String ?{ // This is coroutine?
var?UserName:String? = null
???UserName? = // Do network call to get?user Name?based on?userId
?? return?UserName
}
suspend fun??getUserAge?(Int:userId): Int? { // This is coroutine?
var?UserAge:Int? = null
??? UserAge = // Do network call to get?userAge?based on?userId
?? return?UserAge
}
O/P : Here?async builder will suspend the coroutine (getUserNameFromServer?() and?getUserAge().
async builder will suspend the coroutine (get username from the web server ).?
The execution of other tasks i.e getUserAge() also continues.?
Once??getUserNameFromServer??()?returns a userName, userName is stored in the global variable?.
withContext:?another way of writing the async without writing?await().
Below example (Copied from the android developer?site ):
suspend fun fetchDocs() { ? ? ? ? ? ? ? ? ? ? ?// Dispatchers.Main
? ?val result = get("developer.android.com") ? // Dispatchers.Main
? ?show(result) ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? // Dispatchers.Main
}
suspend fun get(url: String) = ? ? ? ? ? ? ? ? // Dispatchers.Main
? ? withContext(Dispatchers.IO) { ? ? ? ? ? ? ?// Dispatchers.IO (main-safety block)
? ? ? ? /* perform network IO here */ ? ? ? ? ?// Dispatchers.IO (main-safety block)
? ? } ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?// Dispatchers.Main
}
runBlocking and coroutineScope may look similar because they both wait for their body and all its children to complete. The main difference is that the runBlocking method blocks the current thread for waiting, while coroutineScope just suspends, releasing the underlying thread for other usages. Because of that difference, runBlocking is a regular function and coroutineScope is a suspending function.
How can you switch the context from any context to Main or UI thread context in Coroutines ?
Case1:
The withContext(Dispatcher.Main) {} is used in Kotlin coroutines to switch the execution context to the main thread or UI thread. It ensures that the code inside the withContext block is executed on the main thread, allowing you to safely update the user interface or perform UI-related operations.
Here's how it works:
Here's an example usage:
// Inside another coroutine
withContext(Dispatchers.Main) {
? ? // Perform UI-related operations
? ? textView.text = "Hello, World!"
? ? progressBar.visibility = View.VISIBLE
? ? // ...
}
In the above code, the block of code inside the withContext(Dispatchers.Main) is executed on the main thread, allowing you to modify UI components directly. This ensures that UI updates are done on the main thread, preventing any threading-related issues.
It's important to note that the withContext(Dispatcher.Main) should only be used from within a coroutine, as it is a suspending function. It helps in managing thread switching and synchronization within your coroutine-based code.
Case 2:
When you use Dispatchers.IO as the dispatcher in the withContext block, the code inside that block will be executed on the I/O thread rather than the UI/main thread.
Here's an example:
// Inside a coroutine
withContext(Dispatchers.IO) {
? ? // Perform I/O related operations
? ? fetchFromNetwork()
? ? writeToFile()
? ? // ...
}
In the above code, the fetchFromNetwork() and writeToFile() operations are typically blocking I/O operations, such as making network requests or writing data to a file. By using Dispatchers.IO, these operations are executed on a separate I/O thread, which helps keep the UI/main thread responsive and prevents any potential blocking issues.
It's important to note that you should not perform UI-related operations directly within the withContext(Dispatchers.IO) block. If you need to update the UI or perform UI-related tasks, you should switch back to the main thread using withContext(Dispatchers.Main) as shown in the previous example.
// Inside a coroutine
withContext(Dispatchers.IO) {
? ? // Perform I/O related operations
? ? fetchFromNetwork()
? ? writeToFile()
? ? withContext(Dispatchers.Main) {
? ? ? ? // Perform UI-related operations
? ? ? ? textView.text = "Hello, World!"
? ? ? ? progressBar.visibility = View.VISIBLE
? ? ? ? // ...
? ? }
}
By switching to the main thread using withContext(Dispatchers.Main) when necessary, you can safely update UI components or perform other UI-related tasks without blocking the UI/main thread.
---------------------------------------------------------------------------------
CoroutineScope:?--?In Kotlin coroutines, a CoroutineScope provides a way to define a context in which coroutines can be launched. It serves as a container for coroutine instances and provides a way to control their lifecycle.
Here is an example of creating a CoroutineScope:
val myScope = CoroutineScope(Dispatchers.Default)
In this example, we create a new CoroutineScope using the Dispatchers.Default dispatcher. This specifies that all coroutines launched within this scope will use the default dispatcher, which is optimized for CPU-bound tasks.
There are several types of CoroutineScope available in Kotlin:
Here is an example of using a CoroutineScope to launch a coroutine:
val myScope = CoroutineScope(Dispatchers.Default)
myScope.launch {
// Coroutine code here
}
In this example, we launch a new coroutine within the myScope scope using the launch function. The coroutine will use the default dispatcher specified when the scope was created.
It's important to properly manage the lifecycle of CoroutineScope instances and cancel any running coroutines when they are no longer needed. This can be done using the cancel() function on the CoroutineScope, which will cancel all coroutines launched within that scope.
CoroutineScope keeps track of any coroutine it creates using?launch?or?async
In Kotlin, all coroutines run inside a CoroutineScope.
Whenever a new coroutine scope is created, a new job gets created and & associated with it.
A scope controls the lifetime of coroutines through its job.
Every coroutine created using this scope becomes the child of this job.(this is parent & child relation in coroutine)
领英推荐
If any of the coroutines throws an unhandled exception, it’s parent job gets canceled.?Ex: scope.cancel()?.
When a parent job is cancelled ultimately cancels all its children. This is called?structured concurrency Structured concurrency do below three things:
CoroutineScope is an interface that has a single abstract property called coroutineContext. Every coroutine builder (like launch, async, etc.) is an extension on CoroutineScope and inherits its coroutineContext to automatically propagate all its elements and cancellation.
public interface CoroutineScope {
?? public val coroutineContext: CoroutineContext
}
Ex:?On Android, you can use a scope to cancel all running coroutines when, for example, the user navigates away from an?Activity?or?Fragment.
Details of coroutine scope :Kotlin Co-routine Scope | LinkedIn
What is difference between launch and GlobalScope.launch ?
In Kotlin coroutines, launch and GlobalScope.launch are both used to start a new coroutine. However, they differ in terms of the scope and lifetime of the created coroutine.
launch creates a coroutine within the scope of the coroutine that it is called from. This means that the new coroutine is a child of the parent coroutine and inherits its context, such as its CoroutineDispatcher and CoroutineExceptionHandler. When the parent coroutine completes, all of its child coroutines are also cancelled.
On the other hand, GlobalScope.launch creates a top-level coroutine that is not tied to any particular scope or context. This means that the coroutine can continue to run even if its parent coroutine or its calling function has completed. However, this also means that the coroutine must explicitly handle its own cancellation and resource cleanup.
It's generally recommended to avoid using GlobalScope.launch in favor of creating coroutines within a more specific and controlled scope, such as within a function or a CoroutineScope. This can help avoid potential issues with long-lived coroutines and resource leaks.
In summary, CoroutineScope provides a way to define a context in which coroutines can be launched and managed. Different types of CoroutineScope are available for different use cases, such as launching coroutines that interact with the UI or are tied to the lifetime of a ViewModel. Proper management of CoroutineScope instances is important for avoiding issues with long-lived coroutines and resource leaks.
Coroutine Context:Coroutines always execute in some context that?is a set of various elements.
Details of coroutine context : Details on Kotlin coroutineContext | LinkedIn
Job?– models a cancellable workflow with multiple states and a life-cycle that culminates in its completion. Launch?returns a?Job?object.
When you?launch?a coroutine, you basically ask the system to execute the code you pass in, using a lambda expression. That code is not executed immediately, but it is, instead, inserted into a queue
A Job is basically a handle to the coroutine in the queue. It only has a few fields and functions, but it provides a lot of extensibility. ne of the fundamental concepts of Kotlin Coroutines is the Job, which represents a piece of work that can be executed concurrently with other jobs. In this article, we will explore the concept of jobs in Kotlin Coroutines and how they can be used to manage concurrency in your application.
What is a Job in Kotlin Coroutines?
A Job in Kotlin Coroutines represents a unit of work that can be executed concurrently with other jobs. It is a lightweight abstraction over a thread and provides a way to manage the lifecycle of asynchronous tasks. A Job is created when you launch a coroutine, and it is responsible for managing the execution of that coroutine.
A Job has a simple API that allows you to start and stop the execution of a coroutine. You can use the Job API to control the execution of a coroutine, such as canceling a coroutine, waiting for its completion, and checking its status.
States of Job :
In Kotlin coroutines, the Job interface represents a cancellable unit of work. It has several states that indicate the progress and completion of the job. The different states of a Job are:
These states reflect the lifecycle of a Job in Kotlin coroutines and provide information about the progress and outcome of the job's execution. Developers can use these states to handle job completion, cancellation, and failure scenarios appropriately.
Here's an example that demonstrates the different states of a Job:
import kotlinx.coroutines.*
fun main() = runBlocking {
val job = launch {
delay(1000)
println("Coroutine executing")
}
println("Job isActive: ${job.isActive}")
println("Job isCompleted: ${job.isCompleted}")
println("Job isCancelled: ${job.isCancelled}")
job.join() // Wait for the job to complete
println("Job isCompleted: ${job.isCompleted}")
println("Job isCancelled: ${job.isCancelled}")
}
/*
Job isActive: true
Job isCompleted: false
Job isCancelled: false
Coroutine executing
Job isCompleted: true
Job isCancelled: false
*/
In the above code, the job is created and launched with a delay of 1000 milliseconds. The initial state of the job is Active. After the delay, the coroutine executes and completes successfully, transitioning the job to the Completed state. Finally, the state of the job is printed before and after it joins to wait for its completion.
Note that the state property of Job is a val property and provides the current state of the job. The state property is not directly available on the Job interface. However, you can obtain the state of a job using the isActive, isCompleted, and isCancelled properties.
How to create a Job in Kotlin Coroutines?
You can create a Job in Kotlin Coroutines by launching a coroutine using the launch builder. The launch builder creates a new coroutine and returns a Job object that represents that coroutine. Here is an example:
val job = GlobalScope.launch {
// coroutine code
}
In this example, we are creating a new coroutine using the launch builder. The GlobalScope object is a predefined object that provides a global scope for launching coroutines. The launch builder returns a Job object that represents the coroutine that we just launched.
Managing the Lifecycle of a Job
Managing the lifecycle of a Job is an essential part of working with Kotlin Coroutines. The Job API provides several methods that allow you to control the execution of a coroutine, such as canceling a coroutine, waiting for its completion, and checking its status.
Cancelling a Job
You can cancel a coroutine by calling the cancel() method on the Job object. When you cancel a coroutine, it will stop executing immediately, and any resources associated with that coroutine will be released. Here is an example:
val job = GlobalScope.launch {
// coroutine code
}
job.cancel()
In this example, we are launching a new coroutine and then immediately canceling it. The cancel() method will stop the execution of the coroutine and release any resources associated with that coroutine.
Waiting for Completion of a Job
You can wait for the completion of a coroutine by calling the join() method on the Job object. The join() method will block the current thread until the coroutine has completed. Here is an example:
val job = GlobalScope.launch {
// coroutine code
}
job.join()
In this example, we are launching a new coroutine and then waiting for it to complete using the join() method. The join() method will block the current thread until the coroutine has completed.
Checking the Status of a Job
You can check the status of a coroutine by calling the isActive property on the Job object. The isActive property will return true if the coroutine is still running and false if it has completed or been canceled. Here is an example:
val job = GlobalScope.launch {
// coroutine code
}
if (job.isActive) {
// coroutine is still running
} else {
// coroutine has completed or been canceled
}
In this example, we are launching a new coroutine and then checking its status using the isActive property. The isActive property will return true if the coroutine is still running and false if it has completed or been canceled.
Job.join blocks the coroutine associated to that job until all the children jobs finish.
scope.launch {
val job = CoroutineScope(Dispatchers.Main).launch {
???? val foo1 = suspendFoo1()
???? val foo2 = suspendFoo2()
???? doSomething(foo1, foo2)
}
job.join()
callOnlyWhenJobAboveIsDone()
}
Here?callOnlyWhenJobAboveIsDone() is called only when?doSomething()?is finished i.e means even?suspendFoo1 and?suspendFoo2 are finished.
When you use the coroutine, do you still need the Handler ?
When you use coroutines, you typically don't need to use a Handler directly. Coroutines provide a higher-level and more concise way to handle asynchronous and concurrent programming compared to traditional approaches like using Handler.
Coroutines allow you to write asynchronous code in a sequential and structured manner, making it easier to understand and maintain. They also provide built-in support for cancellation, exception handling, and various dispatchers for executing code on different threads.
Instead of using a Handler to post or execute code on a specific thread, you can use suspending functions and coroutine builders provided by the Kotlin coroutines library. For example, you can use the withContext function to switch between different dispatchers and execute code on specific threads:
// Example: Perform a network request on the IO thread and update UI on the main thread
// Inside a coroutine
withContext(Dispatchers.IO) {
? ? // Perform network request
? ? val result = performNetworkRequest()
? ? withContext(Dispatchers.Main) {
? ? ? ? // Update UI with the result
? ? ? ? updateUI(result)
? ? }
}
In the above code, the code inside the withContext(Dispatchers.IO) block is executed on the IO thread, and the code inside the nested withContext(Dispatchers.Main) block is executed on the main thread. This allows you to switch between different threads without explicitly dealing with Handler objects.
By leveraging coroutines and the provided coroutine builders and dispatchers, you can write more concise and readable asynchronous code without the need for explicit Handler usage.
Important APIS :
result.await()
3. withContext(Dispatchers.Default)
4.
There is a separate article on the ThreadPool,Thread and co-routine . Please go through => Relation b/w CoRoutine ,Thread & Threadpool | LinkedIn
Further reading:
Coroutine Lib in stdlib:
I hope you will find this article useful in some way !
If you have any questions or suggestions , please leave a comment.
-------
Please follow me if you would like to receive regular updates regarding my work on Android , Cas/DRM, Android TV framework and broadcasting technologies.