Follow

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use
Contact

Why GlobalScope.async seems to be more performant than coroutineScope.async

I am currently developing small application to learn Kotlin (using some Spring Boot features to speed up the work). Unfortunately, I cannot wrap my head around asynchronously ran coroutines returning some results, most likely due to bad understanding of coroutine scopes.

Following code is my ‘main’ method

@Component
class GameRunner() : CommandLineRunner {

    override fun run(vararg args: String) = runBlocking {

        val players = listOf(Player(1),Player(2),Player(3),Player(4))
        val results = mutableListOf<Results>()
        val measureTimeMillis = measureTimeMillis {
            val deferredResults = mutableListOf<Deferred<List<Results>>>()
            for (i in 0 until 250) {
                deferredResults.add(async { executeLadderRound(players.shuffled()) })
            }
            results.addAll(deferredResults.awaitAll().flatten())
        }
        // ... present the results ...
    }
    
    // for the record, this method plays a simulation of a board game that is fully blocking CPU intensive process, there are no async calls, waits or suspensions of any kind
    fun executeLadderRound(players: List<Player>): List<Results>

It uses around 10% of my CPU and finishes in 25 seconds. It feels like even tough I start lots of asynchronous jobs, they seem to be executed sequentially (same time if I remove async altogether).

MEDevel.com: Open-source for Healthcare and Education

Collecting and validating open-source software for healthcare, education, enterprise, development, medical imaging, medical records, and digital pathology.

Visit Medevel

However, if I do this:

@Component
class GameRunner() : CommandLineRunner {

    override fun run(vararg args: String) = runBlocking {

        val players = listOf(Player(1),Player(2),Player(3),Player(4))
        val results = mutableListOf<Results>()
        val measureTimeMillis = measureTimeMillis {
            val deferredResults = mutableListOf<Deferred<List<Results>>>()
            for (i in 0 until 250) {
                deferredResults.add(GlobalScope.async { executeLadderRound(players.shuffled()) })
            }
            results.addAll(deferredResults.awaitAll().flatten())
        }
        // ... present the results ...
    }
    
    // for the record, this method plays a simulation of a board game that is fully blocking CPU intensive process, there are no async calls, waits or suspensions of any kind
    fun executeLadderRound(players: List<Player>): List<Results>

It seems to work exactly how I expect it to. There is a huge gain in performance (down to 5 seconds) and 100% of CPU is used. But why? If run is my ‘main’ method, shouldn’t it be GlobalScope by default?

>Solution :

Since you are using runBlocking the default thread dispatcher is a single-threaded dispatcher. As it says in the documentation for the JVM version:

The default CoroutineDispatcher for this builder is an internal implementation of event loop that processes continuations in this blocked thread until the completion of this coroutine.

Since the scope you’re using is single-threaded, all the child coroutines you created with async have to share that same thread, so they cannot run in parallel.

When you use GlobalScope to launch them instead of the internal CoroutineScope provided to the runBlocking coroutine lambda, they are not child coroutines. They are parentless coroutines created using GlobalScope’s default dispatcher Dispatchers.Default, which has multiple threads, so they can run in parallel.

You can specify a dispatcher for runBlocking to avoid this problem:

override fun run(vararg args: String) = runBlocking(Dispatchers.Default) {
    //...
}

By the way, you can use the map operator to simplify your code:

override fun run(vararg args: String) = runBlocking(Dispatchers.Default) {
    val players = (1..4).map(::Player)
    val results = (0 until 250).map { async { executeLadderRound(players.shuffled()) }
        .awaitAll()
        .flatten()

    // ... present the results ...
}
Add a comment

Leave a Reply

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use

Discover more from Dev solutions

Subscribe now to keep reading and get access to the full archive.

Continue reading