Table of content
- Structured Concurrency - Explain
- Building up Job in Coroutine
- Supervisor Job
- Structured vs Unstructured Concurrency
- GlobalScope
- ViewModelScope
- LifecycleScope
- Summary
Structured Concurrency - Explain
It is a concept which introduces where
- every coroutine needs to be started in a logical scope with a limited life-time.
- Coroutines started in the same scope form a hierarchy.
- A parent job won’t complete, until all of its children have completed.
- Cancelling a parent will cancel all children. Cancelling a child won’t cancel the parent or siblings.
- If a child coroutine fails, the execution is propagated upwards and depending on the job type, either all siblings are cancelled or not.
import androidx.lifecycle.ViewModel
import kotlinx.coroutines.*
class MainViewModel : ViewModel() {
// CoroutineScope associated with the ViewModel
private val viewModelScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
fun performBackgroundTask() {
// Launch a coroutine within the ViewModel scope
viewModelScope.launch {
try {
// Simulate a background task
delay(2000)
println("Background task completed")
} catch (e: CancellationException) {
// Handle cancellation if needed
println("Coroutine was canceled")
}
}
}
override fun onCleared() {
super.onCleared()
// Cancel all coroutines when the ViewModel is cleared (e.g., when the associated UI component is destroyed)
viewModelScope.cancel()
}
}
In the above example :
- The viewModelScope is a CoroutineScope associated with the main thread (Dispatchers.Main).
- The performBackgroundTask function launches a coroutine within the viewModelScope.
- When the ViewModel is cleared (e.g., when the associated UI component is destroyed), viewModelScope.cancel() is called to cancel all coroutines within that scope.
Building up Job in Coroutine
Job()
is a core concept representing a unit of work that can be executed concurrently. It is a handle to a coroutine or a coroutine-like entity and provides control and information about the state of the coroutine.
Key characteristics of a Job include:
- Lifecycle: A Job represents the lifecycle of a coroutine. It can be in one of the following states: Active, Cancelled, or Completed.
- Cancelling: One can cancel a coroutine by calling the cancel function on its associated Job. When a coroutine is canceled, it stops executing, and its state changes to Cancelled.
- Parent-Child Relationship: Coroutines can be structured in a parent-child relationship using Job instances. A child coroutine is typically created with a parent coroutine’s Job, forming a hierarchy.
- Exception Propagation: If an exception occurs inside a coroutine, it is propagated up to its parent coroutine allowing for centralized exception handling.
- Awaiting Completion: One can use the
await()
to suspend a coroutine until its associated Job is completed.
import kotlinx.coroutines.*
fun main() = runBlocking {
val job: Job = launch {
// Coroutine code
delay(1000)
println("Coroutine is completed")
}
println("Main thread is still working")
// Delay to allow the coroutine to complete
delay(2000)
// Check if the coroutine is still active
if (job.isActive) {
println("Coroutine is still active")
} else if (job.isCancelled) {
println("Coroutine was cancelled")
} else if (job.isCompleted) {
println("Coroutine completed")
}
}
In this example:
launch
is used to create a coroutine, and it returns a Job representing the coroutine’s execution.- The main thread continues its work while the coroutine is running concurrently.
- After a
delay
, the coroutine is allowed to complete. - The state of the Job is checked using isActive, isCancelled, and isCompleted properties to determine the outcome of the coroutine.
Using Job provides control over the lifecycle of a coroutine, enabling cancellation, awaiting completion, and managing the hierarchical relationships between coroutines. It’s a fundamental building block in the structured concurrency model.
Supervisor Job
SupervisorJob is a type of Job that is used to supervise the execution of multiple child coroutines. It behaves differently from a regular Job in the way it handles failures and cancellations within its children.
key characteristics of a SupervisorJob:
- Independent Failure: If one of the child coroutines fails (throws an exception), it doesn’t affect the other children or the supervisor itself.
- Cancellation of Children: If a child coroutine is cancelled (either explicitly or due to a failure), it doesn’t automatically cancel the other children or the supervisor.
- Cancellation Propagation: If the supervisor itself is cancelled, it cancels all its children. However, the cancellation of a child doesn’t trigger the cancellation of its siblings or the supervisor.
import kotlinx.coroutines.*
fun main() = runBlocking {
val supervisorJob = SupervisorJob()
val coroutineScope = CoroutineScope(Dispatchers.Default + supervisorJob)
val job1 = coroutineScope.launch {
println("Child coroutine 1 started")
delay(1000)
println("Child coroutine 1 completed")
}
val job2 = coroutineScope.launch {
try {
println("Child coroutine 2 started")
delay(2000)
throw RuntimeException("Something went wrong in coroutine 2")
} catch (e: Exception) {
println("Coroutine 2 failed: ${e.message}")
}
}
job1.join()
job2.join()
supervisorJob.cancel()
}
In the above example :
- Two child coroutines (job1 and job2) are launched within the context of a
SupervisorJob
. - job1 runs successfully, while job2 intentionally throws an exception.
- Even though job2 fails, it doesn’t affect the completion of job1.
- Cancelling the supervisorJob at the end cancels all its children, but the failure of job2 doesn’t stop job1 from completing.
Using a SupervisorJob is useful in scenarios where one has a collection of related coroutines running concurrently, and one wants them to operate independently in terms of failures and cancellations.
Structured vs Unstructured Concurrency
Structured Concurrency | Unstructured Concurrency |
---|---|
Every Coroutine needs to be started in a logical scope with a limited life-time. | Threads are started globally. Developers responsibility to keep track of their lifetime. |
Coroutines started in a scope form a hierarchy. | No hierarchy. Threads run in isolation without any relationship between each other. |
A parent job won’t complete, until all of its children have completed. | All threads run completely independent from each other. |
Cancelling a parent will cancel all children. | No automatic cancellation mechanism. |
If a child coroutine fails, the exception is propagated upwards and depending on the job type, either all siblings are cancelled or not. | No automatic exception handling and cancellation mechanism. |
GlobalScope
GlobalScope is a special instance of :
- CoroutineScope that is not bound to any specific lifecycle or context.
- a top-level scope that exists throughout the entire application
- typically used for global coroutines that have a lifecycle beyond that of a specific component, such as an Activity or Fragment.
Drawbacks of GlobalScope
-
No Lifecycle Awareness: does not have lifecycle awareness, so coroutines launched with it won’t be automatically canceled when a specific lifecycle (e.g., an Android Activity or Fragment) is destroyed. This can lead to memory leaks and unexpected behavior.
-
Risk of Leaked Coroutines: Coroutines launched in GlobalScope can outlive the components that started them. If a coroutine is still active after its intended scope is destroyed, it can lead to resource leaks.
ViewModelScope
viewModelScope is a predefined CoroutineScope provided by the Android Architecture Components library,
- specifically designed for use in ViewModels.
- It is an extension of the CoroutineScope that is tied to the lifecycle of a ViewModel.
- The viewModelScope is automatically canceled when the associated ViewModel is cleared, which typically happens when the associated UI component (e.g., Activity or Fragment) is destroyed.
class NetworkRequestViewModel(
private val api MockApi = mockApi()
): BaseViewModel<UiState> {
fun performNetworkRequest(timeout: Long){
val job= viewModelScope.launch{
//will get this line as as output
Timber.d("I am the first statement in the coroutine")
try{
val versions = withTimeout(timeout){
api.getRecentAndroidVersions()
}
uiState.value = UiState.Success(versions)
} catch(exception: Exception){
Timber.e(exception)
uiState.value = UiState.Error("Network request failed")
}
}
// will print this line after first Timber statement
Timber.d("I am the first statement after launching the coroutine")
job.invokeOnCompletion{ throwable ->
if(throwable is CancellationException) {
Timber.d("Coroutine was cancelled")
}
}
}
}
LifecycleScope
- lifecycleScope extension property provided by the AndroidX Lifecycle library in conjunction with coroutines.
- This extension property is available on LifecycleOwner components, such as Fragment or Activity, and provides a convenient way to launch coroutines that are automatically tied to the lifecycle of the component.
import android.os.Bundle
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
class MainFragment : Fragment() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Launch a coroutine in the lifecycleScope
lifecycleScope.launch {
try {
// Simulate a background task
delay(2000)
println("Background task completed")
} catch (e: Exception) {
// Handle exceptions if needed
println("Coroutine failed: ${e.message}")
}
}
}
}
In the above example :
- The lifecycleScope.launch is used to launch a coroutine within the scope of the fragment’s lifecycle.
- The coroutine simulates a background task using delay.
- If the fragment is destroyed (e.g., during a configuration change), the coroutine launched in lifecycleScope is automatically canceled.
Summary
Coroutines provide a way to write asynchronous, non-blocking code in a more sequential and readable manner. They are built on top of the Kotlin programming language and leverage its language features to simplify asynchronous programming.
Happy Learning !!!