Coroutines in Kotlin

𝐖𝐡𝐲 𝐚𝐫𝐞 𝐊𝐨𝐭𝐥𝐢𝐧 𝐜𝐨𝐫𝐨𝐮𝐭𝐢𝐧𝐞𝐬 𝐥𝐢𝐠𝐡𝐭𝐰𝐞𝐢𝐠𝐡𝐭 𝐜𝐨𝐦𝐩𝐚𝐫𝐞𝐝 𝐭𝐨 𝐉𝐚𝐯𝐚 𝐭𝐡𝐫𝐞𝐚𝐝𝐬? 


What exactly is thread?

A thread is a single sequential flow of execution of tasks of a process in an operating system (OS).

OS threads are at the core of Java’s concurrency model. Java threads are just a wrapper at OS thread.

There is a one-to-one correspondence between Java threads and system kernel threads, and the thread scheduler of the system kernel is responsible for scheduling Java threads. (As shown in the image)





When you create a new Java thread, the JVM must work with the underlying OS to create and manage that thread, which is quite expensive because the JVM must communicate with the OS back and forth throughout the thread's lifetime.
This switching is an expensive operation, which makes threads expensive, and you can't launch a large number of threads (e.g., 10,000) at once.

However, because Kotlin coroutines are entirely managed by the language (and ultimately by the programmer), they do not require communication with the underlying OS to be created and managed. This eliminates the need to work with the operating system. This is why coroutines are lightweight and can be launched millions at once.


Fun fact:

To avoid this three-decade-long pain, Java 19 introduced the concept of virtual threads. Virtual threads are lightweight threads that are managed by the JVM rather than the OS. Java 19 includes it as a preview feature. You can start a virtual thread as shown below.

        Thread.startVirtualThread(() -> {
         System.out.println("Hello, Project Loom!");
        });


Here are few other commonly asked Kotlin Coroutines interview questions and answers to prepare you for your interview:


1. What are coroutines?

Coroutines are a type of light-weight thread that can be used to improve the performance of concurrent code. Coroutines can be used to suspend and resume execution of code blocks, which can help to avoid the overhead of creating and destroying threads. 

A coroutine in Kotlin programming language is the feature that helps to converts asynchronous background processes to the sequential code.

  • Managing background threads.
  • It helps in thread concurrency efficiently.
  • Replace traditional callbacks.
  • Maps the async code into the sequential code.
  • Main thread safety.


2. Can you explain the difference between a thread and a coroutine?

A thread is a unit of execution that can run independently from other threads. A coroutine is a light-weight thread that can be suspended and resumed.


3. How do threads compare with coroutines in terms of performance?

Threads are typically heavier than coroutines, so they can be more expensive in terms of performance. However, this is not always the case, and it really depends on the specific implementation. In general, coroutines tend to be more efficient when it comes to CPU usage, but threads may be better when it comes to I/O bound tasks.


4. Does Kotlin have any built-in support for concurrency? If yes, then what is it?

Yes, Kotlin has built-in support for concurrency via coroutines. Coroutines are light-weight threads that can be used to improve the performance of concurrent code.


5. Why are coroutines important?

Coroutines are important because they allow you to write asynchronous code that is more readable and easier to reason about than traditional asynchronous code. Coroutines also have the ability to suspend and resume execution, which can make your code more efficient.


6. What makes coroutines more efficient than threads?

Coroutines are more efficient than threads because they are lightweight and can be suspended and resumed without incurring the overhead of a context switch. This means that they can be used to perform tasks that would otherwise block a thread, without incurring the same performance penalty.


7. Can you explain what suspend functions are?

Suspend functions are functions that can be paused and resumed at a later time. This is useful for long-running tasks that might need to be interrupted, such as network requests. By using suspend functions, you can ensure that your code is more responsive and can avoid potential errors. 

By syntax, a function that has a keyword suspend is a suspended function. A suspended function is a part of Kotlin coroutine, that is callable from a coroutine or other suspended functions.


8. Are suspend functions executed by default on the main thread or some other one?

Using suspend doesn't tell Kotlin to run a function on a background thread. It's normal for suspend functions to operate on the main thread. It's also common to launch coroutines on the main thread. You should always use withContext() inside a suspend function when you need main-safety, such as when reading from or writing to disk, performing network operations, or running CPU-intensive operations.


9. Is it possible to use coroutines outside Android development? If yes, then how?

Yes, it is possible to use coroutines outside of Android development. One way to do this is by using the kotlinx-coroutines-core library. This library provides support for coroutines on multiple platforms, including the JVM, JavaScript, and Native.


10. What’s the difference between asynchronous code and concurrent code?

Asynchronous code is code that can run in the background without blocking the main thread. Concurrent code is code that can run in parallel with other code.


11. What happens if we call a suspend function from another suspend function?

When we call a suspend function from another suspend function, the first function will suspend execution until the second function completes. This can be used to our advantage to create asynchronous code that is easy to read and debug.


12. Can you give me an example of when you would want to run two tasks concurrently instead of sequentially?

There are a few reasons you might want to run two tasks concurrently instead of sequentially. One reason is if the tasks are independent of each other and can be run in parallel. Another reason is if one task is dependent on the other task, and you want to avoid blocking the main thread. Finally, if you have a limited number of resources available, you might want to run tasks concurrently in order to make better use of those resources.


13. What is the difference between launch() and async()? Which should be used in certain situations?

The main difference between launch() and async() is that launch() will create a new coroutine and start it immediately, while async() will create a new coroutine but will not start it until something calls await() on the resulting Deferred object. 

In general, launch() should be used when you want a coroutine to run in the background without blocking the main thread, while async() should be used when you need to wait for the result of a coroutine before continuing.


14. What is the best way to cancel a running job in Kotlin?

The best way to cancel a running job in Kotlin is to use the cancel() function. This function will cancel the job and any associated children's jobs.


class SampleClass {

// Job and Dispatcher are combined into a CoroutineContext

val scope = CoroutineScope(Job() + Dispatchers.Main)

fun exampleMethod() {
// Starts a new coroutine within the scope
scope.launch {
// New coroutine that can call suspend functions
fetchDocs()
}
}

fun cleanUp() {
// Cancel the scope to cancel ongoing coroutines work
scope.cancel()
}
}


15. What do you understand about Job objects? How are they different from CoroutineScope?

Job objects are the basic building blocks of coroutines. They define a coroutine’s lifecycle and provide a way to cancel it. CoroutineScope is used to define a scope for a coroutine, which determines its lifetime and other properties.


16. What is coroutineScope and types of CoroutineScope?

Scope in Kotlin’s coroutines can be defined as the restrictions with-in which the Kotlin coroutines are being executed. Scopes help to predict the lifecycle of the coroutines. 

There are basically 3 scopes in Kotlin coroutines:

  1. GlobalScope - coroutines defined with this scope can remain alive throughout app life cycle. If not used properly this scope can lead to memory leakage. 
  2. lifecycleScope - Used in Fragment or Activity and remains active while the activity or fragment is alive.
  3. viewModelScope - Tightly coupled with view model.


17. What happens if there’s an exception thrown inside a coroutine?

If there’s an exception thrown inside a coroutine, then the coroutine will be cancelled. All the coroutine’s children will also be cancelled, and any pending work in those coroutines will be lost.


18. Describe the optimistic usages for different Dispatchers in Kotlin coroutines. 

  • Main: It is used to perform UI kind operations like Main thread in Android. This is important because some operations can only be performed on the main thread, and so specifying that a coroutine should run on the main thread ensures that it will be able to perform those operations.
  • IO: The IO dispatcher is optimized for I/O work like reading from the network or disk. It is used when we want to block threads with I/O (input-output) operations. In a nutshell, anything related to file systems or networking should be done using Dispatchers.IO as those tasks are IO-related tasks. Examples where this dispatcher can be used: 
    • Any network operations like making a network call.
    • Downloading a file from the server.
    • Moving a file from one location to another on disk.
    • Reading from a file.
    • Writing to a file.
    • Making a database query.
    • Loading the Shared Preferences.

    • Default: The Default dispatcher is optimized for CPU intensive tasks. As the name suggests if we forget to choose our dispatcher, this dispatcher will be selected by default. Examples where this dispatcher can be used:
      • Doing heavy calculations like Matrix multiplications.
      • Doing any operations on a bigger list present in the memory like sorting, filtering, searching, etc.
      • Applying the filter on the Bitmap present in the memory, NOT by reading the image file present on the disk.
      • Parsing the JSON available in the memory, NOT by reading the JSON file present on the disk.
      • Scaling the bitmap already present in the memory, NOT by reading the image file present on the disk.
      • Any operations on the bitmap that are already present in the memory, NOT by reading the image file present on the disk.
    • Unconfined: An unconfined dispatcher isn’t restricted to a particular thread. This dispatcher contrasts with those mentioned earlier as it changes no threads. At the point when it is begun, it runs on the thread on which it was begun.


    19: What is are differences between usage of Dispatcher.IO and Dispatcher.Default ?

    The difference is that Dispatchers.Default is limited to the number of CPU cores (with a minimum of 2) so only N (where N == cpu cores) tasks can run in parallel in this dispatcher.

    On the IO dispatcher there are by default 64 threads, so there could be up to 64 parallel tasks running on that dispatcher.

    The idea is that the IO dispatcher spends a lot of time waiting (IO blocked), while the Default dispatcher is intended for CPU intensive tasks, where there is little or no sleep.


    20. What are some common mistakes made when working with coroutines in Kotlin?

    One common mistake is not using the right context when launching a coroutine. This can lead to your coroutine being cancelled when it shouldn’t be, or not being able to access the data it needs. 

    Another mistake is not using a structured concurrency approach, which can lead to race conditions and other issues. 

    Finally, not using the right tools for debugging can make it difficult to find and fix problems with your coroutines.


    21. What are some good practices to follow when using Kotlin coroutines?

    Some good practices to follow when using Kotlin coroutines include:

    1. Use coroutines for short-lived background tasks.
    2. Use coroutines for tasks that can be executed in parallel.
    3. Use coroutines for tasks that need to be executed on a different thread than the UI thread.
    4. Do not use coroutines for tasks that need to be executed on the UI thread.
    5. Do not use coroutines for tasks that need to be executed synchronously.


    22. What is your opinion on the future of coroutines in Kotlin?

    I believe that coroutines will continue to be a popular feature of Kotlin, as they offer a convenient and efficient way to manage concurrency. Additionally, the Kotlin team has been very supportive of coroutines and is constantly working to improve the experience of using them.


    23. What is the difference between CoroutineScope & ViewModelScope?

    CoroutineScope:

    CoroutineScope is the API available in Kotlin Coroutine to create a coroutine and all coroutines run inside a CoroutineScope. A scope controls the lifetime of coroutines through its job. When you cancel the job of scope, it cancels all coroutines started in that scope.

    ViewModelScope:

    It’s available in the below library implementation and it is specific to Android.

    androidx.lifecycle:lifecycle-ViewModel-ktx:x.x.x

    This library has added it as viewModelScope as an extension function of the ViewModel class.

    This scope is bound to Dispatchers.Main and will automatically be canceled when the ViewModel is cleared.


    24. What is the difference between withTimeout & withTimeoutOrNull functions. What is their basic purpose of use?

    The basic purpose or the use for both of them is to start a coroutine with a time out constraints. It's the easiest way to constraint a coroutine with a time out.

    withTimeout:

    On timeout, the exception will be a thrown & we need to handle the exception explicitly otherwise will be propagated to the caller.

            try {
             withTimeout(1_000) {
            val job = launch(Dispatchers.Default) {
             // Some long running operations
             }
             }
            } catch (e: Exception) {
             println(e.message)
            }


    withTimeoutOrNull:

    This withTimeoutOrNull function that is similar to withTimeout but it returns null on timeout instead of throwing an exception. No need to use exception handling in this case.

       withTimeoutOrNull(1_000){
    val job = launch(Dispatchers.Default) {
    // Your code
    }
        }



    25. What are coroutines channels?

    A channel is conceptually similar to a queue. A channel has a suspending send function and a suspending receive function. This means that several coroutines can use channels to pass data to each other in a non-blocking fashion.

    One or more producer coroutines write to a channel. One or more consumer coroutines can read from the same channel. 

    Unlike a queue, a channel can be closed to indicate that no more elements are coming. On the receiver side it is convenient to use a regular for loop to receive elements from the channel.

        val channel = Channel<Int>()
    launch {
    // we'll send eight squares
    for (i in 1..8) channel.send(i * i)
    channel.close() // we're done sending
    }
    // here we print eight received integers:
    repeat(5) { println(channel.receive()) }
    println("Done!")
        }

    There are four types of channels, and they differ in the number of values they can hold at a time.

    1. Buffered Channel - As the name suggests, a buffered channel has a predefined buffer. We can specify the capacity of the buffer in the Channel constructor.
    2. Unlimited Channel - An unlimited channel has a buffer of unlimited capacity. But we should be aware that we may run into OutOfMemoryError if the buffer overloads and all of the available memory is exhausted. We can create an unlimited channel by providing the special constant UNLIMITED to the Channel constructor.
    3. Rendezvous Channel - A rendezvous channel has no buffer. The sending coroutine suspends until a receiver coroutine invokes receive on the channel. Similarly, a consuming coroutine suspends until a producer coroutine invokes send on the channel. We create a rendezvous channel using the default Channel constructor with no arguments.
    4. Conflated Channel - In a conflated channel, the most recently written value overrides the previously written value. Therefore, the send method of the channel never suspends. The receive method receives only the latest value.


    26. What is the difference between buffered & unbuffered channels?

    Briefly, Channels are the structures used for communication between coroutines.

    As we know for async coroutine builder deferred values provide a convenient way to transfer a single value between coroutines. Channels provide a way to transfer a stream of values.

    Un-Buffered Channels:

    By default, the channels have no buffer. Unbuffered channels transfer elements when the sender and receiver meet each other. If send is invoked first, then it is suspended until receive is invoked, if receive is invoked first, it is suspended until send is invoked.

    Buffered Channels:

    Both Channel() factory function and produce builder take an optional capacity parameter to specify buffer size. Buffer allows senders to send multiple elements before suspending, similar to the BlockingQueue with a specified capacity, which blocks when the buffer is full.

        val channel = Channel<Int>(4) // create buffered channel
    val sender = launch { // launch sender coroutine
    repeat(10) {
    println("Sending $it") // print before sending each element
    channel.send(it) // will suspend when buffer is full
    }
    }
    // don't receive anything... just wait....
    delay(1000)
    sender.cancel() // cancel sender coroutine



    27. What are the differences between sharedflow and stateflow?

    Flow is cold! means it emits data only when it is collected. Also, Flow cannot hold data, take it as a pipe in which water is flowing, data in flow only flows, not stored (no .value function).

    Unlike flow, stateflow and sharedflow are hot streams means they emit data even when there is no collector. Also, if there are multiple collectors, a new flow will be run for each collector, completely independent from each other. SharedFlow and StateFlow are Flows that allows for sharing itself between multiple collectors, so that only one flow is effectively run for all of the simultaneous collectors. If you define a SharedFlow that accesses databases and it is collected by multiple collectors, the database access will only run once, and the resulting data will be shared to all collectors.


    StateFlow : 

    Stateflow takes an initial value through constructor and emits it immediately when someone starts collecting. Stateflow is identical to LiveData. LiveData automatically unregisters the consumer when the view goes to the STOPPED state. When collecting a StateFlow this is not handled automatically, you can use repeatOnLifeCyCle scope if you want to unregister the consumer on STOPPED state. If you want current state, use stateflow (.value).


    SharedFlow:

    StateFlow only emits last known value, whereas sharedflow can configure how many previous values to be emitted. If you want emitting and collecting repeated values, use sharedflow.


    In summary, use StateFlow when you need to manage and share a single state with multiple collectors, and use SharedFlow when you need to share a stream of events or signals among collectors without holding any state.




    Comments

    Popular posts from this blog

    Most Relevant Android Interview Questions 2023

    Cheat Sheet for Kotlin