Learn through the super-clean Baeldung Pro experience:
>> Membership and Baeldung Pro.
No ads, dark-mode and 6 months free of IntelliJ Idea Ultimate to start with.
Last updated: March 19, 2024
In this article, we’ll see what the coroutine scope and coroutine context are and what are the purposes and usage of each of them.
In brief, the coroutine context is a holder of data related to the coroutine, while the coroutine scope is a holder of the coroutine context. Now, let’s take a closer look at the difference between coroutine scope and coroutine context.
To launch a coroutine, we need to use a coroutine builder like launch or async. These builder functions are actually extensions of the CoroutineScope interface. So, whenever we want to launch a coroutine, we need to start it in some scope.
The scope creates relationships between coroutines inside it and allows us to manage the lifecycles of these coroutines. There are several scopes provided by the kotlinx.coroutines library that we can use when launching a coroutine. There’s also a way to create a custom scope. Let’s have a look.
One of the simplest ways to run a coroutine is to use GlobalScope:
GlobalScope.launch {
delay(500L)
println("Coroutine launched from GlobalScope")
}
The lifecycle of this scope is tied to the lifecycle of the whole application. This means that the scope will stop running either after all of its coroutines have been completed or when the application is stopped.
It’s worth mentioning that coroutines launched using GlobalScope do not keep the process alive. They behave similarly to daemon threads. So, even when the application stops, some active coroutines will still be running. This can easily create resource or memory leaks.
Another scope that comes right out of the box is runBlocking. From the name, we might guess that it creates a scope and runs a coroutine in a blocking way. This means it blocks the current thread until all childrens’ coroutines complete their executions.
It is not recommended to use this scope because threads are expensive and will depreciate all the benefits of coroutines.
The most suitable place for using runBlocking is the very top level of the application, which is the main function. Using it in main will ensure that the app will wait until all child jobs inside runBlocking complete.
Another place where this scope fits nicely is in tests that access suspending functions.
For all the cases when we don’t need thread blocking, we can use coroutineScope. Similarly to runBlocking, it will wait for its children to complete. But unlike runBlocking, this scope doesn’t block the current thread but only suspends it because coroutineScope is a suspending function.
Consider reading our companion article to find more details about the differences between runBlocking and coroutineScope.
There might be cases when we need to have some specific behavior of the scope to get a different approach in managing the coroutines. To achieve that, we can implement the CoroutineScope interface and implement our custom scope for coroutine handling.
Now, let’s take a look at the role of CoroutineContext here. The context is a holder of data that is needed for the coroutine. Basically, it’s an indexed set of elements where each element in the set has a unique key.
The important elements of the coroutine context are the Job of the coroutine and the Dispatcher.
Kotlin provides an easy way to add these elements to the coroutine context using the “+” operator:
launch(Dispatchers.Default + Job()) {
println("Coroutine works in thread ${Thread.currentThread().name}")
}
A Job of a coroutine is to handle the launched coroutine. For example, it can be used to wait for coroutine completion explicitly.
Since Job is a part of the coroutine context, it can be accessed using the coroutineContext[Job] expression.
Another important element of the context is Dispatcher. It determines what threads the coroutine will use for its execution.
Kotlin provides several implementations of CoroutineDispatcher that we can pass to the CoroutineContext:
Sometimes, we must change the context during coroutine execution while staying in the same coroutine. We can do this using the withContext function. It will call the specified suspending block with a given coroutine context. The outer coroutine suspends until this block completes and returns the result:
newSingleThreadContext("Context 1").use { ctx1 ->
newSingleThreadContext("Context 2").use { ctx2 ->
runBlocking(ctx1) {
println("Coroutine started in thread from ${Thread.currentThread().name}")
withContext(ctx2) {
println("Coroutine works in thread from ${Thread.currentThread().name}")
}
println("Coroutine switched back to thread from ${Thread.currentThread().name}")
}
}
}
The context of the withContext block will be the merged contexts of the coroutine and the context passed to withContext.
When we launch a coroutine inside another coroutine, it inherits the outer coroutine’s context, and the job of the new coroutine becomes a child job of the parent coroutine’s job. Cancellation of the parent coroutine leads to cancellation of the child coroutine as well.
We can override this parent-child relationship using one of two ways:
In both cases, the new coroutine will not be bound to the scope of the parent coroutine. It will execute independently, meaning that canceling the parent coroutine won’t affect the new coroutine.
In this article, we’ve seen the difference between coroutine scope and coroutine context, their purposes, and usage.
We learned that scope is used to create and manage the coroutine, and it’s responsible for the coroutine’s lifecycle. At the same time, the coroutine context is a holder of data represented as a set of elements that are associated with the coroutine. Job and Dispatcher are important elements of this set that define how to execute the coroutine.