Sitemap

Android Coroutine

22 min readJul 16, 2024

--

About Coroutine:

A coroutine is a concurrency design pattern that you can use in Android to simplify the code that will executes your asynchronously task.

A coroutine in Kotlin is a lightweight, concurrent code block that can be paused and resumed, allowing you to write asynchronous code that looks and behaves like synchronous code.

Unlike traditional threads, coroutines are not bound to any specific thread and can be suspended and resumed at any point, enabling non-blocking operations. This leads to more efficient and scalable code.

Coroutines in Kotlin provide a powerful and flexible way to handle asynchronous programming, making your code more readable and maintainable.

On Android, coroutines help to manage long-running tasks that might otherwise block the main thread and cause your app to become unresponsive

Key Concepts of Coroutines

  1. Suspend Functions: A suspend function is a function that can be paused and resumed at a later time without blocking the thread on which it is running . It is designed to be used within coroutines and allows you to perform long-running operations or non-blocking asynchronous tasks in a sequential, readable manner.
    They are marked with the suspend keyword and can only be called from another suspend function or a coroutine.
suspend fun fetchUser(): User {
// Simulate fetching user
delay(500)
return User("John Doe")
}

suspend fun fetchUserProfile(user: User): UserProfile {
// Simulate fetching user profile
delay(500)
return UserProfile(user.name, "Developer")
}

fun main() = runBlocking {
val user = fetchUser()
val profile = fetchUserProfile(user)
println(profile)
}

2. Coroutine Builders: Functions like launch and async are used to create and start a coroutine. launch is used for coroutines that don't return a result, while async is used for coroutines that return a result through Deferred.

  • runBlocking: Starts a coroutine and blocks the current thread until it completes. It is used for bridging coroutines and non-coroutine code.
  • launch: Starts a coroutine and returns a Job. It is used for side effects and fire-and-forget tasks.
  • async: Starts a coroutine and returns a Deferred that can be awaited for results. It is used for concurrent computations.
  • withContext: Changes the coroutine's context for a block of code, allowing context switching.
  • produce: Creates a coroutine that produces values to a channel and returns a ReceiveChannel.
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.*

fun main() = runBlocking {

launch {
delay(1000L) // Simulate some work
println("Hello from coroutine!")
}
println("Hello from main!")



val deferred: Deferred<Int> = async {
delay(1000L) // Simulate some work
42
}
println("The answer is ${deferred.await()}")


withContext(Dispatchers.IO) {
// This block runs in the IO dispatcher context
delay(1000L)
println("Hello from IO dispatcher!")
}
println("Back to main thread!")



val numbers = produceNumbers()
repeat(5) {
println(numbers.receive()) // Receives numbers from the channel
}
println("Done!")


fun CoroutineScope.produceNumbers(): ReceiveChannel<Int> = produce {
for (i in 1..5) {
delay(500L)
send(i)
}
}
}

/** Below output

Hello from main!

Hello from lunch coroutine!

Hello from async coroutine!

The answer is 42

Hello from withContext & IO dispatcher!

Back to main thread!

1
2
3
4
5

Done!
*/

3. Coroutine Scope: It’s define the context and lifecycle of coroutines. Commonly used scopes include GlobalScope, lifecycleScope, CoroutineScope, and viewModelScope in Android.

  • CoroutineScope: General-purpose scope for managing coroutines.
  • runBlocking: Blocks the current thread until the coroutine completes, useful for bridging non-coroutine and coroutine code and widally using in unit testing.
  • GlobalScope: Provides a global coroutine scope that lasts for the application's lifetime.
  • CoroutineScope with SupervisorJob: Allows child coroutines to fail independently.
  • viewModelScope: Tied to the lifecycle of a ViewModel, automatically cancels coroutines when the ViewModel is cleared.
  • lifecycleScope: Tied to the lifecycle of Android components like Activities and Fragments, automatically cancels coroutines when the lifecycle is destroyed.
fun main() = runBlocking {

// Example of CoroutineScope
val scope = CoroutineScope(Dispatchers.Default)
scope.launch {
// Launches a coroutine in the provided context
delay(1000L)
println("Hello from CoroutineScope!")
}
Thread.sleep(2000L) // Wait for the coroutine to complete


// Example of lifecycleScope
lifecycleScope.launch {
delay(1000L)
viewmodel.fetchData()
}


// Example of GlobalScope
GlobalScope.launch {
delay(1000L)
println("Hello from GlobalScope!")
}

Thread.sleep(2000L) // Wait for the coroutine to complete


// Example of CoroutineScope
val supervisor = CoroutineScope(SupervisorJob() + Dispatchers.Default)
supervisor.launch {
delay(500L)
println("Child coroutine 1")
throw RuntimeException("Failure in child 1")
}
supervisor.launch {
delay(1000L)
println("Child coroutine 2")
}
delay(1500L) // Ensure all coroutines complete

}



class MyViewModel : ViewModel() {
fun fetchData() {
// Example of viewModelScope
viewModelScope.launch {
delay(1000L)
println("Data fetched in ViewModel!")
}
}
}

4. Coroutine Context and Dispatchers : Coroutines run within a coroutine context, which can include information about the thread or thread pool where the coroutine is executing. For instance, Dispatchers.IO is often used for I/O-bound tasks, while Dispatchers.Main is used for UI operations. The context helps manage where the coroutine resumes after suspension.

The following standard implementations are provided by kotlinx.coroutines as properties on the Dispatchers object:

  • Dispatchers.Default : is used by all standard builders if no dispatcher or any other ContinuationInterceptor is specified in their context. It uses a common pool of shared background threads. This is an appropriate choice for compute-intensive coroutines that consume CPU resources.
  • Dispatchers.IO: uses a shared pool of on-demand created threads and is designed for offloading of IO-intensive blocking operations (like file I/O and blocking socket I/O).
  • Dispatchers.Unconfined: starts coroutine execution in the current call-frame until the first suspension, whereupon the coroutine builder function returns. The coroutine will later resume in whatever thread used by the corresponding suspending function, without confining it to any specific thread or pool. The Unconfined dispatcher should not normally be used in code.
  • newSingleThreadContext: Private thread pools can be created with newSingleThreadContext and newFixedThreadPoolContext.
import kotlinx.coroutines.*

fun main() = runBlocking<Unit> {
launch { // context of the parent, main runBlocking coroutine
println("main runBlocking : I'm working in thread ${Thread.currentThread().name}")
}
launch(Dispatchers.Unconfined) { // not confined -- will work with main thread
println("Unconfined : I'm working in thread ${Thread.currentThread().name}")
}
launch(Dispatchers.Default) { // will get dispatched to DefaultDispatcher
println("Default : I'm working in thread ${Thread.currentThread().name}")
}
launch(Dispatchers.IO) { // will get dispatched to DefaultDispatcher
println("IO : I'm working in thread ${Thread.currentThread().name}")
}
launch(newSingleThreadContext("MyOwnThread")) { // will get its own new thread
println("newSingleThreadContext: I'm working in thread ${Thread.currentThread().name}")
}
}

/** Below Output
main runBlocking : I'm working in thread main @coroutine#2

Unconfined : I'm working in thread main @coroutine#3

Default : I'm working in thread DefaultDispatcher-worker-1 @coroutine#4

IO : I'm working in thread DefaultDispatcher-worker-1 @coroutine#5

newSingleThreadContext: I'm working in thread MyOwnThread @coroutine#6
*/

Coroutine Scope in details:

Type Of Coroutine Scope

1. LifeCycleScole:

It’s Allows you to launch coroutines within the lifecycle of Android components like Activities and Fragments. This helps in managing the lifecycle of coroutines, ensuring they are automatically canceled when the lifecycle owner is destroyed or reaches an appropriate state.

Using LifecycleScope helps manage coroutines in a lifecycle-aware manner, preventing potential issues like memory leaks and ensuring that your coroutines only run when the component is active.

Integrated with Android’s Lifecycle to launch coroutines that automatically cancel when the lifecycle is destroyed. Useful for activities and fragments

Example: In Android, lifecycleScope is automatically tied to the lifecycle of an Activity or Fragment. Coroutines are automatically cancelled when the Activity or Fragment is destroyed.

import androidx.lifecycle.*
import kotlinx.coroutines.*

class MyActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycleScope.launch {
println("LifecycleScope Coroutine started")
delay(1000L)
println("LifecycleScope Coroutine finished")
}
}
}

2. viewModelScope:

Lifecycle Awareness: Coroutines launched in viewModelScope are tied to the lifecycle of the ViewModel. This means that when the ViewModel is cleared, all coroutines in its scope are automatically canceled.

Prevents Memory Leaks: By automatically canceling coroutines when the ViewModel is cleared, viewModelScope helps prevent memory leaks.

Example: In Android, viewModelScope is tied to the lifecycle of the ViewModel. Coroutines launched within this scope are cancelled when the ViewModel is cleared.

import androidx.lifecycle.*
import kotlinx.coroutines.*

class MyViewModel : ViewModel() {
fun startWork() {
viewModelScope.launch {
println("ViewModelScope Coroutine started")
delay(1000L)
println("ViewModelScope Coroutine finished")
}
}
}

3. GlobalScope:

GlobalScope in Kotlin coroutines is a global CoroutineScope that is not tied to any specific lifecycle. Coroutines launched in GlobalScope are long-lived and live for the entire lifetime of the application unless explicitly canceled.

The coroutine runs independently of the main thread. It is not tied to any lifecycle, so it will continue running until it completes or is manually cancelled.

Example: The coroutine runs independently of the main thread. It is not tied to any lifecycle, so it will continue running until it completes or is manually cancelled

fun main() {
GlobalScope.launch {
println("GlobalScope Coroutine started")
delay(1000L)
println("GlobalScope Coroutine finished")
}
}

4. CoroutineScope:

A more controlled scope typically used in classes and components. It allows for better management of coroutine lifecycle in a specific context.

Example: Here, a CoroutineScope is created with a specific context (Dispatchers.Default + Job()). The scope manages the lifecycle of coroutines launched within it. The cleanup function cancels all coroutines in the scope.

import kotlinx.coroutines.*

class MyClass {
private val scope = CoroutineScope(Dispatchers.Default + Job())

fun startWork() {
scope.launch {
println("Coroutine started in MyClass")
delay(1000L)
println("Coroutine finished in MyClass")
}
}

fun cleanup() {
scope.cancel() // Cancels all coroutines in this scope
}
}

fun main() {
val myClass = MyClass()
myClass.startWork()
Thread.sleep(500L) // Simulate some work
myClass.cleanup() // Cancels coroutine before it completes
}

Coroutine Builder in details

Type Of Coroutine Builder:

1. launch:

Launches a new coroutine without blocking the current thread and returns a reference to the coroutine as a Job.

The launch coroutine builder is used to start a new coroutine that does not return a result. It returns a Job object, which can be used to manage the coroutine.

The coroutine is cancelled when the resulting job is cancelled.
Use launch for coroutines that do not return a result.

Use “join” with “Job” (coroutine job object/reference) from “launch” for Wait for coroutine Completion

Pros of Using launch

  • Simple and Intuitive: Easy to Use: launch provides a straightforward way to start coroutines. It is intuitive for tasks that don’t need to return a result.
  • Minimal Boilerplate: It simplifies concurrent code by handling threading and synchronization internally.
  • Structured Concurrency: Scope Management: Coroutines launched with launch are tied to the lifecycle of the scope in which they are launched. This helps in managing coroutine cancellation and ensures proper resource cleanup.
  • Structured Concurrency: Automatic Cancellation: If the parent scope is cancelled, all child coroutines launched in that scope are also cancelled, preventing orphaned tasks.
  • Flexible Context: Context Specification: You can specify the CoroutineContext (e.g., Dispatchers.IO, Dispatchers.Default, Dispatchers.Main) to determine where the coroutine should run, allowing you to optimize for various types of tasks.
  • Non-Blocking: Concurrency: Coroutines launched with launch are non-blocking and can run concurrently with other coroutines and tasks. They don’t block the thread on which they run.
  • Control Over Execution: Immediate or Lazy Start: You can control whether the coroutine starts immediately or lazily (when explicitly started), giving you flexibility in how and when tasks are executed.

Cons of Using launch

  • No Result Handling: No Return Value: Unlike async, launch does not return a result. If you need to compute and return a value from a coroutine, you need to use async instead.
  • Error Handling Complexity: Exception Propagation: Uncaught exceptions within a launch coroutine do not propagate to the caller. You need to handle exceptions explicitly within the coroutine or rely on structured concurrency to manage errors.
  • Error Handling Complexity: Potential for Silent Failures: If exceptions are not handled properly, they can lead to silent failures, especially in cases where coroutines are running in the background.
  • Resource Management: Potential for Resource Leaks: If not managed correctly, coroutines might lead to resource leaks. For example, if a coroutine is launched but not properly cancelled when its scope is no longer needed, it may continue running or hold onto resources.
  • Difficulty in Debugging: Concurrency Issues: Debugging issues with concurrent code can be more challenging. Coroutines are asynchronous, and issues like race conditions or deadlocks can be harder to diagnose compared to synchronous code.
  • Complex Cancellation Handlingl: Cancellation Overhead: In some scenarios, cancelling coroutines may involve overhead and complexity, particularly if coroutines are interacting with external resources or long-running task
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.*
fun main() = runBlocking {

/* Basic Usage
Basic Usage with Handling Results
*/
var result = 0
val job1 = launch {
result = performComputation()
}

val job2 = launch {
delay(2000L)
}

// Wait for the job1 computation to finish
job1.join()

println("Result: $result")

// Wait for job2 to complete
job2.join()


println("All coroutines completed")



/* Handling Exceptions - try catch
Handling Exceptions using try catch
*/
val job3 = launch {
try {
throw RuntimeException("Exception in coroutine")
} catch (e: Exception) {
println("Caught exception: ${e.message}")
}
}
job3.join() // Wait for the coroutine to finish



/* Handling Exceptions - SupervisorJob
Supervision and Exception Handling
*/
val supervisor = SupervisorJob()
val scope = CoroutineScope(Dispatchers.Default + supervisor)

val job5 = scope.launch {
try {
throw RuntimeException("Exception in job 5")
} catch (e: Exception) {
println("Job1 caught exception: ${e.message}")
}
}

val job6 = scope.launch {
delay(1000L)
println("Job6 completed")
}

job5.join() // Wait for job1 to complete
job6.join() // Wait for job2 to complete
println("All jobs completed")



/*
Starting Coroutines - Lazily
1. You can use the CoroutineStart.LAZY option to start a coroutine lazily, meaning it won’t start until explicitly started
*/
val job7 = launch(start = CoroutineStart.LAZY) {
delay(1000L)
println("Lazily started coroutine")
}
println("Coroutine not started yet")
job7.start() // Explicitly start the coroutine
job7.join() // Wait for it to complete




/* Cancelling Coroutines - cancel()
1. Coroutines launched with launch can be cancelled by calling cancel() on the Job returned by launch.
*/
val job8 = launch {
try {
repeat(1000) { i ->
delay(500L)
println("Coroutine iteration $i")
}
} catch (e: CancellationException) {
println("Coroutine cancelled")
}
}
delay(1500L) // Let the coroutine run for a while
job8.cancel() // Cancel the coroutine
job8.join() // Ensure the coroutine has been cancelled




/* Start Menually, wait for completion and On Completion
*/
val jobx = launch(start = CoroutineStart.LAZY) {
println("Coroutine execution started.")
delay(1000)
println("Coroutine execution completed.")
}

jobx.invokeOnCompletion {
println("Coroutine has completed.")
}

println("Starting the coroutine now...")
jobx.start()

jobx.join() // Wait for the coroutine to complete
println("Main program done.")



//launch with Timeout
try {
withTimeout(2000L) { // Timeout after 2 seconds
val job1 = launch { delay(1000L); println("Task 1 completed") }
val job2 = launch { delay(1500L); println("Task 2 completed") }
val job3 = launch { delay(3000L); println("Task 3 completed") } // Will timeout

job1.join()
job2.join()
job3.join()
}
println("All tasks completed successfully.")
} catch (e: TimeoutCancellationException) {
println("Timed out while waiting for tasks to complete.")
}



val success = withTimeoutOrNull(2000L) { // Timeout after 2 seconds
val job1 = launch { delay(1000L); println("Task 1 completed") }
val job2 = launch { delay(1500L); println("Task 2 completed") }
val job3 = launch { delay(3000L); println("Task 3 completed") } // Will timeout

job1.join()
job2.join()
job3.join()
true // Return success if all tasks finish
}

if (success == null) {
println("Timed out while waiting for tasks to complete.")
} else {
println("All tasks completed successfully.")
}


}

suspend fun performComputation(): Int {
delay(1000L)
return 42
}

2. async:

Launches a new coroutine and returns a Deferred that represents a future result. It can be used with await to get the result.

Use async for coroutines that return a result as Deferred Object

  • async: The async coroutine builder is used to start a coroutine that performs a computation and returns a result. It returns a Deferred<T>, which represents a future result that can be awaited.
  • await(): This function is called on the Deferred<T> to retrieve the result of the computation. It suspends until the result is available.
import kotlinx.coroutines.*

fun main() = runBlocking {
/*
Basic Example
*/
val deferred: Deferred<Int> = async {
delay(1000L)
42 // Return value
}


/* Handling Results: Waits for the result and Suspends until the result is available
1. You can use await() to wait for the completion of the result of an async coroutine. It suspends the calling coroutine until the Deferred is resolved.
*/
val result = deferredResult.await()
println("Computed result: $result")


/*
Cancelling Coroutines and Handling Exceptions
1. You can cancel a coroutine that was started with async, which also cancels its result computation. You should handle the cancellation properly.
*/
val deferred2 = async {
try {
repeat(1000) { i ->
delay(500L)
println("Coroutine iteration $i")
}
} catch (e: CancellationException) {
println("Coroutine cancelled")
}catch (e: Exception) {
println("Caught exception: ${e.message}")
}
}
delay(1500L) // Let the coroutine run for a while
deferred2.cancel() // Cancel the coroutine
deferred2.await() // This will throw a CancellationException


/* Joining Coroutines
1. await() is used to join a coroutine started with async by suspending until the result is ready. It’s similar to join() but for coroutines that return a result.
*/
println("Result 1: ${deferredResult.await()}")
println("Result 2: ${deferred2.await()}")



/* Explicitly Resuming the Coroutines - resume()
A suspendCoroutine block suspends a coroutine, and you can explicitly resume it using the provided Continuation object.
*/
val result = suspendCoroutine<String> { continuation ->
// Simulate asynchronous callback or manual resumption
println("Suspending coroutine")
continuation.resume("Resumed coroutine with result")
}

println(result) // This executes after the coroutine is resumed



/* Explicitly Resuming the Coroutines - resume()
CompletableDeferred can act as a placeholder for a coroutine result, and you can resume a coroutine by completing it.*/
val deferred = CompletableDeferred<String>()
launch {
println("Waiting for result...")
val result = deferred.await() // Suspend until the result is available
println("Got result: $result")
}

delay(1000) // Simulate some work
deferred.complete("Resumed with CompletableDeferred result")





/* Start Explicitly
Resuming Explicitly
Completion handling Explicitly
wait & join
*/
val completableDeferred = CompletableDeferred<String>()

val job = launch(start = CoroutineStart.LAZY) {
println("Coroutine started, performing work...")
delay(1000) // Simulate work
completableDeferred.complete("Work Completed!") // Complete the deferred
}

// Add completion handling
job.invokeOnCompletion { throwable ->
if (throwable == null) {
println("Coroutine completed successfully.")
} else {
println("Coroutine completed with an error: ${throwable.message}")
}
}

println("Coroutine is created but not started.")

// Start the coroutine manually
job.start()
println("Coroutine started manually.")

// Await the result from CompletableDeferred
val result = completableDeferred.await()
println("Result: $result")

job.join()
println("All done.")




// async with timeout
val deferred1 = async {
try {
withTimeout(1000L) {
delay(2000L) // This will timeout
"Result from Task 1"
}
} catch (e: TimeoutCancellationException) {
"Task 1 timed out"
}
}
}


val result = withTimeoutOrNull(2000L) { // Timeout after 2 seconds
val deferred1 = async { delay(1000L); "Result from Task 1" }
val deferred2 = async { delay(1500L); "Result from Task 2" }
val deferred3 = async { delay(3000L); "Result from Task 3" } // Will timeout

listOf(deferred1.await(), deferred2.await(), deferred3.await())
}

if (result == null) {
println("Timed out before all tasks could complete.")
} else {
println("All tasks completed successfully: $result")
}

}

3. runBlocking:

This coroutine builder is used to start a new coroutine and block the current thread until its completion.

The runBlocking coroutine builder in Kotlin is used to bridge the gap between synchronous and asynchronous code. It is primarily intended for use in situations where you need to write code that integrates with coroutines but must run in a blocking manner. This is typically used in:

  1. Main Functions: To start coroutines in the main function of a Kotlin application.
  2. Test Code: To write coroutine-based tests that require synchronous execution to ensure correct behavior.
  3. Bridging Synchronous and Asynchronous Code: In scenarios where you need to call suspending functions from regular code, such as in libraries or frameworks that do not yet support coroutines.
import kotlinx.coroutines.*

fun main() = runBlocking {
// Code inside this block runs in the coroutine
launch {
delay(1000L)
println("Hello from coroutine!")
}
println("Hello from main!")
}
import kotlinx.coroutines.*
import kotlin.test.*

class MyTests {

@Test
fun testCoroutine() = runBlocking {
val result = async {
delay(1000L)
42
}.await()
assertEquals(42, result)
}
}

4. Custome: CoroutineScope(Job()+Dispatchers(IO)):

  • CoroutineScope(Dispatchers.IO + SupervisorJob()): Creates a scope for background operations with its own job, which will not be cancelled by other jobs.
  • scope.launch { ... }: Starts a coroutine to fetch data in the background.
  • scope.cancel(): Cancels all coroutines in this scope, useful for cleanup.
import kotlinx.coroutines.*

class DataFetcher {
private val scope = CoroutineScope(Dispatchers.IO + Job())

fun fetchData() {
scope.launch {
// Perform long-running background task
delay(3000L)
println("Data fetched")
}
}

fun cancel() {
scope.cancel() // Cancel all coroutines in this scope
}
}

Some Key Difference between

Job v/s Deferred:

Key Differences between Job and Deferred is like below:

  1. Return Type:
    Job
    : No return value (used for tasks that don’t return a result).
    Deferred: Returns a result (used for tasks that need to return a value).
  2. Creation:
    Job
    : Created using launch.
    Deferred: Created using async.
  3. Waiting for Completion:
    Job
    : Use join() to wait for completion.
    Deferred: Use await() to wait for completion and retrieve the result.
  4. Use Case:
    Job
    : Suitable for fire-and-forget tasks or when you need to perform some work that doesn’t require a result..
    Deferred: Suitable for tasks where you need to get a result after the work is done.

Dispatchers:

In Kotlin coroutines, Dispatchers are used to determine the thread or thread pool where the coroutine will run.

Type of Dispatchers

  1. Dispatchers.Main :
    - Confines the coroutine to the main thread, typically used for updating the UI in Android applications.
    - Suitable for tasks that interact with the UI
  2. Dispatchers.IO:
    - Optimized for offloading blocking I/O tasks, such as network requests, file operations, and database interactions.
    - Uses a shared pool of on-demand created threads.
  3. Dispatchers.Default:
    - Optimized for CPU-intensive tasks such as sorting large lists or complex calculations.
    - Uses a shared pool of threads optimized for CPU usage
  4. Dispatchers.Unconfined:
    - Starts the coroutine in the caller thread but only until the first suspension point. After suspension, it resumes in the thread that is fully determined by the suspending function.
    - Generally not recommended for production code due to its unpredictable behavior.

withContext :

The withContext function is used to switch the coroutine context and is often used for performing blocking operations in a specific context (like IO operations).

The withContext function in Kotlin coroutines is used to change the context in which a coroutine operates. It allows you to switch the coroutine’s execution context to a different dispatcher or CoroutineContext for a specific block of code. This is particularly useful when you need to perform tasks on different threads or contexts, such as moving from the main thread to a background thread for I/O operations.

How withContext Works

  • Context Switching: withContext switches the coroutine’s execution context for the duration of the block of code inside it. Once the block completes, the coroutine returns to its original context.
  • Suspending Function: withContext is a suspending function, meaning it can only be called from within a coroutine or another suspending function. It suspends the coroutine while switching contexts and then resumes it in the new context.
  • Non-Blocking: It does not block the thread but suspends the coroutine, allowing other coroutines or tasks to run concurrently.
import kotlinx.coroutines.*
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import android.widget.TextView

class MainActivity : AppCompatActivity() {

private lateinit var textView: TextView

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
textView = findViewById(R.id.textView)

// Launch a coroutine in the MainScope
CoroutineScope(Dispatchers.Main).launch {
val result = withContext(Dispatchers.IO) {

// Simulate a long-running task
delay(2000L)
"Data fetched"
}
textView.text = result // Update UI on the main thread
}
}
}

Coroutine Exception Handler

SupervisorJob and supervisorScope

In Kotlin coroutines, SupervisorJob and supervisorScope are tools designed to manage and handle the failure of child coroutines more gracefully, providing finer control over coroutine supervision and error handling.

SupervisorJob

SupervisorJob is a special type of Job that allows child coroutines to fail independently without affecting their sibling coroutines or the parent coroutine. This is in contrast to the default behavior of a regular Job, where the failure of any child coroutine cancels all other children and the parent coroutine.

Key Characteristics:

  • Isolated Failure: Failures of child coroutines do not cancel other sibling coroutines or the parent coroutine.
  • Error Handling: Allows for more flexible error handling, as you can decide how to handle errors from individual child coroutines without affecting others.
import kotlinx.coroutines.*

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

val scope = CoroutineScope(Dispatchers.Default + supervisor)

val job1 = scope.launch {
try {
delay(1000L)
throw RuntimeException("Error in job1")
} catch (e: Exception) {
println("Handled error in job1: ${e.message}")
}
}

val job2 = scope.launch {
delay(2000L)
println("Job2 completed")
}

val jobx = launch {
delay(1000L)
throw RuntimeException("Error in job1")
}

job1.join() // Wait for job1 to complete
jobx.join() // Wait for job1 to complete
job2.join() // Wait for job2 to complete
}

yield() v/s delay() :

yield() and delay() are both suspending functions in Kotlin coroutines and use for pouse the execution with difference way.

yield()

Purpose: yield() temporarily releases the thread (or dispatcher) and gives other coroutines a chance to run. It allows for cooperative multitasking without specifying a duration for the suspension.

Get Abhishek Rai’s stories in your inbox

Join Medium for free to get updates from this writer.

Behavior: yield() suspends the coroutine and immediately resumes it, allowing other coroutines that are ready to run on the same dispatcher to execute.

Use Case: Used when you want to ensure that other coroutines get a chance to run, especially during long-running computations, without specifying a wait time.

delay()

Purpose: delay() suspends the coroutine for a specified amount of time. It is a non-blocking way to pause the execution of the coroutine.

Behavior: delay() suspends the coroutine for a given duration and resumes it after the delay period has elapsed. It does not block the underlying thread.

Use Case: Used when you need to pause the coroutine for a specific duration, such as simulating a timer or waiting for an event.

delay() v/s sleep() :

delay() and sleep() are both methods used to pause the execution of a program, but they function differently and are used in different contexts

Key Differences
Context:
delay(): Used within coroutines and designed for asynchronous programming.
sleep(): Used within threads and suitable for traditional multi-threading.

Blocking vs. Non-blocking:
delay(): Non-blocking; suspends the coroutine without blocking the thread, allowing other coroutines to run.
sleep(): Blocking; blocks the entire thread, preventing other tasks from running on that thread during the sleep period.

Usage:
delay(): Should be used when working with coroutines to introduce a delay without blocking the thread.
sleep(): Can be used in traditional threading contexts where blocking the thread is acceptable.

Performance:
delay(): More efficient in a coroutine context because it doesn’t block the thread, allowing for better resource utilization.
sleep(): Less efficient in scenarios where you want to maintain high concurrency and responsiveness, as it blocks the thread.

Threads vs. Coroutines

Threads

Definition: Threads are the smallest unit of a process that can be scheduled and executed by the operating system. They are a fundamental unit of execution in multi-threaded programming.

OS Managed: Threads are managed by the operating system.

Heavyweight: Creating and managing threads can be resource-intensive. Each thread has its own stack and consumes a significant amount of memory.

Blocking: Threads can be blocked, meaning if one thread is performing a blocking operation (like I/O), it will halt its execution until the operation completes.

Concurrency: Threads enable concurrency by allowing multiple threads to run in parallel, potentially on different CPU cores.

Context Switching: Switching between threads (context switching) can be expensive in terms of performance because the CPU must save the state of the current thread and load the state of the next thread.

Coroutines

Definition: Coroutines are a concurrency design pattern that you can use on the JVM to simplify code that executes asynchronously. They are lightweight and designed to handle a large number of tasks with fewer resources.

Language Managed: Coroutines are managed by the Kotlin runtime and the language itself, not the operating system.

Lightweight: Coroutines are much lighter than threads. They are not tied to any specific thread and can be suspended and resumed efficiently.

Non-blocking: Coroutines are non-blocking by design. They allow you to write asynchronous code that looks like synchronous code, without blocking threads.

Concurrency: Coroutines provide concurrency without the need for multiple threads. A single thread can execute multiple coroutines by suspending and resuming them.

Structured Concurrency: Coroutines support structured concurrency, making it easier to handle tasks hierarchically and ensuring that resources are properly managed and cleaned up.

Key Differences:

Management:
Threads: Managed by the OS.
Coroutines: Managed by the Kotlin runtime and language.

Resource Consumption:
Threads: Heavyweight, high memory consumption per thread.
Coroutines: Lightweight, low memory consumption.

Concurrency Model:
Threads: True parallelism, each thread can run on a separate core.
Coroutines: Cooperative multitasking, concurrency is achieved by suspending and resuming coroutines.

Blocking v/s Non-blocking:

Threads: Can block.
Coroutines: Non-blocking.

Context Switching:
Threads: Expensive context switching.
Coroutines: Efficient suspension and resumption.

Ease of Use:
Threads: More complex error handling and resource management.
Coroutines: Simplified error handling, structured concurrency.

Performance:
Threads: Suitable for CPU-bound tasks requiring true parallelism.
Coroutines: Suitable for I/O-bound tasks and scenarios where you need a large number of concurrent operations without the overhead of threads.

Threads

Whta is Thread:

A thread is the smallest unit of execution within a process.

It represents a single sequence of instructions that can be managed independently by a scheduler.

Threads are a fundamental concept in concurrent programming, allowing multiple sequences of operations to be performed simultaneously within a single process.

Key Characteristics of a Thread

Independence: Each thread has its own execution path but shares the process’s resources like memory and file handles.

Concurrency: Threads enable a process to perform multiple operations concurrently. This means that tasks can be interleaved, and in the case of multi-core processors, they can even run in parallel.

Shared Memory: Threads within the same process share the same address space. This allows them to easily share data, but it also requires careful synchronization to prevent data corruption.

Lightweight: Compared to processes, threads are lighter in terms of resource usage. Creating and managing threads incurs less overhead than doing the same for processes.

Context Switching: Threads can be switched in and out of execution by the scheduler, which is less expensive than switching between processes because threads share the same memory space.

Example:

class MyThread extends Thread {
public void run() {
System.out.println("Thread is running");
}
}



class MyRunnable implements Runnable {
public void run() {
System.out.println("Runnable is running");
}
}



public class Main {
public static void main(String[] args) {

MyThread myThread = new MyThread();
myThread.start(); // Start the thread

Thread myRunnableThread = new Thread(new MyRunnable());
myRunnableThread.start(); // Start the thread
}
}

Thread Scheduler

The thread scheduler is a part of the Java Virtual Machine (JVM) that works with the OS to determine which thread should run at any given moment.

The scheduler uses various algorithms to decide the execution order of threads, including:

1) Time-Slicing (Round-Robin): The scheduler allocates a small unit of time (time slice) to each thread and cycles through them. This gives the appearance that threads are running simultaneously.

2) Priority-Based Scheduling: Each thread has a priority (a number between Thread.MIN_PRIORITY and Thread.MAX_PRIORITY). The scheduler prefers to run higher-priority threads over lower-priority ones

3) Preemptive Scheduling : The scheduler can interrupt a running thread to give another thread a chance to execute, especially if a higher-priority thread becomes runnable.

Detailed Thread Lifecycle

New: A thread is created using the Thread class or implementing the Runnable interface. At this stage, the thread is not yet started.

Runnable: After invoking the start() method on the thread object, the thread enters the runnable state. It is eligible for execution but not necessarily running. The thread scheduler can move it between runnable and running states based on CPU availability and priority.

Running: When the thread scheduler selects a thread from the runnable pool, it enters the running state and executes the run() method. The actual work of the thread happens here.

Blocked/Waiting: Thread is waiting for a resource or another thread.
1. Blocked: A thread enters the blocked state when it is waiting for a monitor lock to enter or reenter a synchronized block/method.
2. Waiting: A thread enters the waiting state when it is waiting indefinitely for another thread to perform a particular action (e.g., a thread waits on Object.wait()).
3. Timed Waiting: A thread enters this state when it is waiting for a specified period. This can happen due to methods like Thread.sleep(long millis), Object.wait(long timeout), Thread.join(long millis), etc.

Terminated: Thread has completed execution and is no longer active. The thread reaches the terminated state when it has completed its execution or has been aborted due to an unhandled exception. Once a thread is terminated, it cannot be restarted

Important Methods of Thread

start() : Starts the execution of the thread. The JVM calls the thread’s run() method

thread.start();

run() : The JVM calls the thread’s run() method. Contains the code that constitutes the new thread’s task. This method should be overridden in the Thread class or passed as a Runnable to the thread.

public void run() { /* */ }

sleep(long millis) : Causes the current thread to suspend execution for a specified period.

hread.sleep(1000);

join() : Waits for the thread to die. It allows one thread to wait for the completion of another.

thread.join();

yield() : Causes the currently executing thread object to temporarily pause and allow other threads to execute.

thread.yield();

wait() : Causes the current thread to wait until another thread invokes the notify() or notifyAll() method on the same object

synchronized (sharedObject) {
sharedObject.wait();
}

notify() : Wakes up a single thread that is waiting on the object’s monitor. If multiple threads are waiting, one of them is chosen (not guaranteed which one).

synchronized (sharedObject) {
sharedObject.notify();
}

notifyAll() : Wakes up all the threads that are waiting on the object’s monitor. All waiting threads become runnable but will compete for the monitor lock.

synchronized (sharedObject) {
sharedObject.notifyAll();
}

--

--