1. Overview

In this tutorial, we’ll compare two methods for launching Kotlin coroutinesrunBlocking and coroutineScope.

2. Maven Dependency

Before we can use coroutines, we need to add the kotlinx-coroutines-core dependency to our pom.xml:

<dependency>
    <groupId>org.jetbrains.kotlinx</groupId>
    <artifactId>kotlinx-coroutines-core</artifactId>
    <version>1.6.2</version>
</dependency>

In the following sections, we’ll examine how runBlocking and coroutineScope differ in launching, suspending, and canceling coroutines.

3. Launching Coroutines

Both runBlocking and coroutineScope are coroutine builders, which means they are used to launch coroutines, but we use them in different contexts.

When we use coroutineScope to build and launch a coroutine, we create a suspension point. Suspension points are places in the code where Kotlin may suspend the current coroutine. However, we cannot create a suspension point when there is nothing to suspend, so we cannot invoke coroutineScope outside the scope of an existing coroutine.

We sometimes need to launch a coroutine from outside an existing coroutine scope, such as from the main method or a unit test. This is where we would use runBlocking, which bridges blocking and suspendable code. We can invoke runBlocking outside the scope of any existing coroutine. As its name suggests, the coroutine launched by runBlocking will block the current thread; it does not create a suspension point, even if used inside another coroutine.

4. Suspending Coroutines

In the last section, we saw how runBlocking and coroutineScope differ in whether they can suspend the coroutine that contains them if one exists. Here, we’ll look at whether the coroutines launched by the two builders can themselves be suspended.

4.1. Suspending coroutineScope Coroutines

The coroutines launched by coroutineScope are suspendable. To see this, let’s begin by creating a coroutine dispatcher using a fixed thread pool with two threads:

val context = Executors.newFixedThreadPool(2).asCoroutineDispatcher()

Next, let’s define a function that will start ten coroutines, each of which will launch a child coroutine using coroutineScope:

fun demoWithCoroutineScope() = runBlocking {
    (1..10).forEach {
        launch(context) {
            coroutineScope {
                println("Start No.$it in coroutineScope on ${Thread.currentThread().name}")
                delay(500)
                println("End No.$it in coroutineScope on ${Thread.currentThread().name}")
            }
        }
    }
}

Above, we start with runBlocking to create a bridge from our blocking code. From inside the runBlocking block, we use launch to dispatch suspendable coroutines to any inactive thread in the context‘s thread pool. Finally, we use coroutineScope to launch a coroutine that will invoke delay() with 500 milliseconds. The delay() method is a suspending function that creates a suspension point for the coroutine launched by coroutineScope.

Let’s invoke this function to observe how Kotlin can suspend these coroutines:

fun main() {
    val coroutineScopeTimeInMills = measureTimeMillis {
        demoWithCoroutineScope()
    }
    println("coroutineScopeTimeInMills = $coroutineScopeTimeInMills")
}

Here is the output from an example run:

Start No.1 in coroutineScope on pool-1-thread-1
Start No.2 in coroutineScope on pool-1-thread-2
Start No.3 in coroutineScope on pool-1-thread-2
Start No.4 in coroutineScope on pool-1-thread-2
Start No.5 in coroutineScope on pool-1-thread-2
Start No.6 in coroutineScope on pool-1-thread-2
Start No.7 in coroutineScope on pool-1-thread-1
Start No.9 in coroutineScope on pool-1-thread-1
Start No.8 in coroutineScope on pool-1-thread-2
Start No.10 in coroutineScope on pool-1-thread-1
End No.1 in coroutineScope on pool-1-thread-2
End No.2 in coroutineScope on pool-1-thread-1
End No.3 in coroutineScope on pool-1-thread-1
End No.4 in coroutineScope on pool-1-thread-1
End No.5 in coroutineScope on pool-1-thread-2
End No.6 in coroutineScope on pool-1-thread-1
End No.7 in coroutineScope on pool-1-thread-2
End No.9 in coroutineScope on pool-1-thread-1
End No.8 in coroutineScope on pool-1-thread-2
End No.10 in coroutineScope on pool-1-thread-1
coroutineScopeTimeInMills = 609

Despite each coroutine invoking delay() with 500 milliseconds, our total runtime was not much more than that. We can also see that some coroutines were started on pool-1-thread-1 but completed on pool-1-thread-2, and vice versa.

Kotlin was able to suspend each coroutine at the delay() invocation’s suspension point. Since a suspended coroutine does not block any threads, another coroutine could step in, use the thread to launch its own delay() invocation, and then also be suspended. After the 500ms delay, each coroutine could resume on any inactive thread in the pool.

4.2. Suspending runBlocking Coroutines

Next, let’s try replacing coroutineScope with runBlocking:

fun demoWithRunBlocking() = runBlocking {
    (1..10).forEach {
        launch(context) {
            runBlocking {
                println("Start No.$it in runBlocking on ${Thread.currentThread().name}")
                delay(500)
                println("End No.$it in runBlocking on ${Thread.currentThread().name}")
            }
        }
    }
}

This time, we get a very different result:

Start No.1 in runBlocking on pool-1-thread-1
Start No.2 in runBlocking on pool-1-thread-2
End No.1 in runBlocking on pool-1-thread-1
Start No.3 in runBlocking on pool-1-thread-1
End No.2 in runBlocking on pool-1-thread-2
Start No.4 in runBlocking on pool-1-thread-2
End No.4 in runBlocking on pool-1-thread-2
Start No.5 in runBlocking on pool-1-thread-2
End No.3 in runBlocking on pool-1-thread-1
Start No.6 in runBlocking on pool-1-thread-1
End No.5 in runBlocking on pool-1-thread-2
Start No.7 in runBlocking on pool-1-thread-2
End No.6 in runBlocking on pool-1-thread-1
Start No.8 in runBlocking on pool-1-thread-1
End No.8 in runBlocking on pool-1-thread-1
End No.7 in runBlocking on pool-1-thread-2
Start No.9 in runBlocking on pool-1-thread-1
Start No.10 in runBlocking on pool-1-thread-2
End No.9 in runBlocking on pool-1-thread-1
End No.10 in runBlocking on pool-1-thread-2
runBlockingTimeInMills = 2607

Each thread was completed on the same thread it started on, and our total runtime took over 2500ms. The coroutine launched by runBlocking ignored the suspension point created by the delay() call. runBlocking coroutines are not suspendable.

5. Canceling Coroutines

The two coroutine builders also differ in whether their coroutines may be canceled. Let’s look at the cancellation behavior for each builder.

5.1. Canceling coroutineScope Coroutines

We can use the Job reference returned by launch to cancel a coroutine 100ms after launching it:

private fun cancelCoroutineScope() = runBlocking {
    val job = launch {
        coroutineScope {
            println("Start coroutineScope...")
            delay(500)
            println("End coroutineScope...")
        }
    }
    delay(100)
    job.cancel()
}

The cancel() call attempts to cancel the execution of the job. Let’s test this:

fun main() = runBlocking {
    val cancelCoroutineScopeTime = measureTimeMillis {
        cancelCoroutineScope()
    }
    println("cancelCoroutineScopeTime = $cancelCoroutineScopeTime")
}

Here is an example run’s output:

Start coroutineScope...
cancelCoroutineScopeTime = 123

We can see that the coroutine was canceled at delay(500)‘s suspension point shortly after our 100ms delay.

5.2. Canceling runBlocking Coroutines

Let’s do the same thing but using runBlocking in place of coroutineScope:

private fun cancelRunBlocking() = runBlocking {
    val job = launch {
        runBlocking {
            println("Start runBlocking...")
            delay(500)
            println("End runBlocking...")
        }
    }
    delay(100)
    job.cancel()
}

We can test this in the same way:

fun main() = runBlocking {
    val cancelRunBlockingTime = measureTimeMillis {
        cancelRunBlocking()
    }
    println("cancelRunBlockingTime = $cancelRunBlockingTime")
}

As with suspension, we get very different output this time:

Start runBlocking...
End runBlocking...
cancelRunBlockingTime = 519

We can see here that coroutines launched using runBlocking are not cancelable. Since cancellation occurs at suspension points, and runBlocking coroutines are not suspendable and do not have suspension points, the coroutine was allowed to complete its execution.

6. Conclusion

In this article, we learned that runBlocking coroutines could not be suspended or canceled as coroutineScope coroutines can and that runBlocking is our only option when we need to launch coroutines from outside an existing coroutine’s scope.

As always, all of the code in this article is available over on GitHub.

Comments are closed on this article!