1. Overview

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.

2. Coroutine Scope

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.

2.1. GlobalScope

One of the simplest ways to run a coroutine is to use GlobalScope:

GlobalScope.launch {
    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.

2.2. runBlocking

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.

2.3. coroutineScope

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.

2.4. Custom Coroutine Scope

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.

3. Coroutine Context

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}")

3.1. Job in the Context

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.

3.2. Coroutine Context and Dispatchers

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:

  • Dispatchers.Default uses a shared thread pool on the JVM. By default, the number of threads is equal to the number of CPUs available on the machine.
  • Dispatchers.IO is designed to offload blocking IO operations to a shared thread pool.
  • Dispatchers.Main is present only on platforms that have main threads, such as Android and iOS.
  • Dispatchers.Unconfined doesn’t change the thread and launches the coroutine in the caller thread. The important thing here is that after suspension, it resumes the coroutine in the thread that was determined by the suspending function.

3.3. Switching the Context

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.

3.4. Children of a Coroutine

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:

  • Explicitly specify a different scope when launching a new coroutine
  • Pass a different Job object to the context of the new coroutine

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.

4. Conclusion

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.

All examples and code snippets from this tutorial can be found over on GitHub.