Kotlin async not running tasks parallelly

98 Views Asked by At

I was trying Kotlin coroutines using a test file. What I observed is that the async{..} block , completes and then the next async block starts.

 @Test
  fun `xyz`() {

    runBlocking {
      val x = async {
        x()
      }
      val y = async {
        y()
      }
      val z = async {
        z()
      }
      println("x, y, z -> ${x.await()} , ${y.await()}, ${z.await()} ")
    }


  }

  fun x() = "x".also {
    repeat(1000) {
      println("waiting for network response.")
    }
    println("returning x")
  }

  fun y() = "y".also { println("returning y") }
  fun z() = "z".also { println("returning z") }

In this code block I can see "returning y" and "returning z" only gets printed after the call to x() completes. Thats not how I expect Kotlin to run things parallelly. Could you please tell me the change I need to do to run the three calls concurrently or not wait for one to complete. PS - The code in test method can be run directly on main as well. :)

1

There are 1 best solutions below

10
João Esperancinha On BEST ANSWER

This is an interesting question and the reason why you are not getting the expected asynchronous behavior comes from your runBlocking function and the BlockingEventLoop it creates as context. When running coroutines with this dispatcher, you will only work in a single Thread. Though technically async launches coroutines asynchronously, for your example, you'll only see this behaviour when performing non blocking operations like delay and this is only because delay schedules but this is of course not what you are looking for. The scope contexts you may be interested in for async to work are IO, Default or Unconfined, but I think in your case Default applies best. Keep in mind that unit tests usually need to start a blocking context like with runBlocking or runTest and so using a coroutineScope may work only if you are already in a coroutine scope (i.e. in a suspend function or withing a scope started with runBlocking) and if the context is already non-blocking. Here are 3 different versions of your own code with 3 different examples: a blocking one and two non-blocking with 2 different styles.

@Test
fun `should xyz synchronously in spite of async`() {
    runBlocking {
        val x = async {
            "x".also {
                repeat(1000) {
                    println("waiting for network response.")
                }
                println("returning x")
            }
        }
        val y = async {
            "y".also { println("returning y") }
        }
        val z = async {
            "z".also { println("returning z") }
        }
        println("x, y, z -> ${x.await()} , ${y.await()}, ${z.await()} ")
        println(coroutineContext)
    }
}

@Test
fun `should xyz asynchronously because of non-blocking with explicit Dispatchers_IO`() {
    runBlocking {
            withContext(Dispatchers.Default) {
                val x = async {
                    "x".also {
                        repeat(1000) {
                            println("waiting for network response.")
                        }
                        println("returning x")
                    }
                }
                val y = async {
                    "y".also { println("returning y") }
                }
                val z = async {
                    "z".also { println("returning z") }
                }
                println("x, y, z -> ${x.await()} , ${y.await()}, ${z.await()} ")
                println(coroutineContext)
            }
    }
}
@Test
fun `should xyz asynchronously because of non-blocking  with coroutineScope`():Unit = runBlocking {
            coroutineScope {
                CoroutineScope(Dispatchers.Default).launch {
                    val x = async {
                        "x".also {
                            repeat(1000) {
                                println("waiting for network response.")
                            }
                            println("returning x")
                        }
                    }
                    val y = async {
                        "y".also { println("returning y") }
                    }
                    val z = async {
                        "z".also { println("returning z") }
                    }
                    println("x, y, z -> ${x.await()} , ${y.await()}, ${z.await()} ")
                    println(coroutineContext)
                }.join()
            }

}

Note that, as suggested by @Joffrey, although the examples are used to make clear points about contexts and scopes in Kotlin coroutines, the following example would be the most correct one to implement in regards to respecting a concept very important in coroutines called structured concurrency. This is a bit more complex concept to understand, but in general we should avoid context switching when it is not necessary:

@Test
fun `should xyz asynchronously because of non-blocking with explicit Dispatchers_IO`()  = runBlocking(Dispatchers.Default){
            val x = async {
                "x".also {
                    repeat(1000) {
                        println("waiting for network response.")
                    }
                    println("returning x")
                }
            }
            val y = async {
                "y".also { println("returning y") }
            }
            val z = async {
                "z".also { println("returning z") }
            }
            println("x, y, z -> ${x.await()} , ${y.await()}, ${z.await()} ")
            println(coroutineContext)
        }

Also make sure to consider using assertions in these tests. They are not complete. They are only focused on anwering your question.