Coroutines in Kotlin

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:

  1. Simplicity: Coroutines are easy to understand and use, especially compared to low-level threading APIs. They can be written using familiar language constructs like function calls and control flow statements, making them accessible to developers of all skill levels.
  2. Efficiency: Coroutines are lightweight and have minimal overhead, allowing large numbers of them to be created and executed without consuming excessive system resources.
  3. Asynchrony: Coroutines can be used to write asynchronous and non-blocking code, allowing long-running operations to be performed without blocking the main thread or tying up system resources.
  4. Structured concurrency: Coroutines have built-in support for structured concurrency, which ensures that coroutines are started, executed, and cancelled in a predictable and safe way. This makes it easier to write robust and error-resistant code.
  5. Exception handling: Coroutines have built-in support for exception handling, allowing exceptions to be propagated up the call stack to the appropriate handler. This makes it easier to write code that is resilient to errors and can gracefully recover from failures.

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.

No alt text provided for this image


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.

  1. Can be used to perform an asynchronous task which returns a value and achieves parallel execution .
  2. This value in Kotlin terms is a Deferred<T> value?which is the equivalent of a JavaScript promise.
  3. We can call?await?on the deferred value in order to wait and get the result.
  4. Use async for results from multiple tasks that run in parallel.

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 will return a Deferred value as the result of??getUserNameFromServer??()?by calling the suspending function?await()?on it.?
  • await() will fetch us the result that??getUserNameFromServer??()/ async returns.
  • While??getUserNameFromServer??()?is executing, the following happens:

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?.

  • Step 1 to 3 will be executed for getUserAge() also.
  • Once both userName and UserAge is fetched, it will be printed.?

withContext:?another way of writing the async without writing?await().

  1. Use?withContext?when you do not need the parallel execution.
  2. Both?withContext?and async used to get the result which is not possible with the launch.
  3. Use?withContext?to return the result of a single task.

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:

  1. The withContext function is a suspending function provided by Kotlin coroutines. It allows you to switch the execution context to a specific dispatcher.
  2. The Dispatcher.Main represents the main thread dispatcher, which is associated with the main/UI thread of the Android application. It ensures that the code inside the withContext block is executed on the main thread.
  3. By wrapping your code inside the withContext(Dispatcher.Main) {} block, you can safely perform UI-related operations, such as updating UI components, modifying views, or accessing UI-related resources.

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:

  1. GlobalScope - This scope is used to launch top-level coroutines that are not tied to any specific scope or context. It should generally be avoided in favor of more specific scopes.
  2. MainScope - This scope is specifically designed for use in Android applications, and is tied to the main UI thread. It should only be used for launching coroutines that will interact with the UI.
  3. ViewModelScope - This scope is tied to a ViewModel instance in an Android application, and can be used to launch coroutines that are tied to the lifetime of the ViewModel.
  4. CoroutineScope - This is a general-purpose scope that can be used to launch coroutines within a specific context.

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:

  1. Cancel work?when it is no longer needed.
  2. Keep track?of work while it’s running.
  3. Signal errors?when a coroutine fails.

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:

  1. New: The initial state of a job when it is created, before it starts executing.
  2. Active: The job is actively running and performing its work.
  3. Completed: The job has completed its execution successfully without any exceptions.
  4. Cancelling: The job is in the process of being cancelled. It is transitioning from the active state to the cancelled state.
  5. Cancelled: The job has been explicitly cancelled or its parent job has been cancelled. Cancellation is cooperative, meaning the coroutine code should check for cancellation and respond appropriately.
  6. Failed: The job has completed exceptionally with an unhandled exception. This state occurs when an exception is thrown within the coroutine and not properly handled.
  7. Completing: The job is in the process of completing. It is transitioning from the active state to the completed state.
  8. Inactive: The job is no longer active or running. It can be in the completed, cancelled, or failed state.

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 is essentially a task that runs in background, and it can be interpreted as an?action, with a?lifecycle?that gets?destroyed once finished.
  • You can even establish a hierarchy, with parents and child jobs, so that when you cancel the father, the children also get cancelled.
  • We can run different operations using jobs:

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 :

  1. val job = scope.launch{} // job.join(), job.canmcel()
  2. val result = async{} // Returns Deferred<T> which has await() API

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.

要查看或添加评论,请登录

Amit Nadiger的更多文章

  • Rust modules

    Rust modules

    Referance : Modules - Rust By Example Rust uses a module system to organize and manage code across multiple files and…

  • List of C++ 17 additions

    List of C++ 17 additions

    1. std::variant and std::optional std::variant: A type-safe union that can hold one of several types, useful for…

  • List of C++ 14 additions

    List of C++ 14 additions

    1. Generic lambdas Lambdas can use auto parameters to accept any type.

    6 条评论
  • Passing imp DS(vec,map,set) to function

    Passing imp DS(vec,map,set) to function

    In Rust, we can pass imp data structures such as , , and to functions in different ways, depending on whether you want…

  • Atomics in C++

    Atomics in C++

    The C++11 standard introduced the library, providing a way to perform operations on shared data without explicit…

    1 条评论
  • List of C++ 11 additions

    List of C++ 11 additions

    Referance : C++11 - cppreference.com 1.

    2 条评论
  • std::lock, std::trylock in C++

    std::lock, std::trylock in C++

    std::lock - cppreference.com Concurrency and synchronization are essential aspects of modern software development.

    3 条评论
  • std::unique_lock,lock_guard, & scoped_lock

    std::unique_lock,lock_guard, & scoped_lock

    C++11 introduced several locking mechanisms to simplify thread synchronization and prevent race conditions. Among them,…

  • Understanding of virtual & final in C++ 11

    Understanding of virtual & final in C++ 11

    C++ provides powerful object-oriented programming features such as polymorphism through virtual functions and control…

  • Importance of Linux kernal in AOSP

    Importance of Linux kernal in AOSP

    The Linux kernel serves as the foundational layer of the Android Open Source Project (AOSP), acting as the bridge…

    1 条评论

社区洞察

其他会员也浏览了