How to force rest calls in parallel using Kotlin coroutines?

Viewed 697

I have looked around and found this explaining how to execute code in parallel, however my code is running in a Spring Boot app, not on Android, so I believe the dispatchers should be different.

At any rate, the following code does not run in parallel (only runs on a single thread -- http-nio-8080-exec-1 -- in sequence). So, it's taking over 4 seconds when it should take a little more than 2 seconds:

import kotlinx.coroutines.*
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RestController
import org.springframework.web.client.RestTemplate

@RestController
class CoroutineController {

    @GetMapping("/")
    suspend fun find(): String {
        var result = ""
        coroutineScope {
            val start = System.currentTimeMillis()
            println(Thread.currentThread().name + "   starting at " + start)
            val product = async {
                findById()
            }
            val product2 = async {
                findById2()
            }
             result = product.await() + product2.await()
            println(Thread.currentThread().name + "   ending  " + (System.currentTimeMillis() - start))
        }
        return result
    }

    suspend fun findById(): String {
        println(Thread.currentThread().name + "   starting find 1")
        val restTemplate = RestTemplate()
        val response = restTemplate.getForObject("https://httpbin.org/ip", String::class.java)
        Thread.sleep(2000)
        return response!!
    }

    suspend fun findById2(): String {
        println(Thread.currentThread().name + "   starting find 2")
        val restTemplate = RestTemplate()
        val response = restTemplate.getForObject("https://httpbin.org/ip", String::class.java)
        Thread.sleep(2000)
        return response!!
    }

}

Output in console:

http-nio-8080-exec-1   starting at 1607845427335
http-nio-8080-exec-1   starting find 1
http-nio-8080-exec-1   starting find 2
http-nio-8080-exec-1   ending  4783

How to best make the calls findById and findById2 to run in parallel (i.e. on separate threads) so as to save loading time?

2 Answers

Looks like simply appending (Dispatchers.Default) to one of the asyncs forces parallelism. However I'm open to other answers or critiques, as to how best to offload IO tasks to a background thread in a non-Android Kotlin app.

Example working code:

import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.runBlocking

fun main() {
    runBlocking {
        val res = find()
        println("result is $res")
    }
}

suspend fun find(): String {
    var result = ""
    coroutineScope {
        val start = System.currentTimeMillis()
        println(Thread.currentThread().name + "   starting at " + start)
        val product = async(Dispatchers.Default) {
            findById()
        }
        val product2 = async {
            findById2()
        }
        result = product.await() + product2.await()
        println(Thread.currentThread().name + "   ending  " + (System.currentTimeMillis() - start))
    }
    return result
}
fun findById(): String {
    println(Thread.currentThread().name + "   starting find 1")
    val response = "a"
    Thread.sleep(2000) //make IO call here
    return response
}

fun findById2(): String {
    println(Thread.currentThread().name + "   starting find 2")
    val response = "b"
    Thread.sleep(2000) //make IO call here
    return response
}

Result:

main   starting at 1608844986270
DefaultDispatcher-worker-1   starting find 1
main   starting find 2
main   ending  2042
result is ab

Ref: https://kotlinexpertise.com/kotlin-coroutines-concurrency

Might be a little late to answer this question but according to Kotlin docs Dispatcher.Unconfined results most commonly in execution in the same thread as it was invoked from. So mostly it will be main, or in this example a http-nio thread which is a request thread. Overall Dispatcher.Unconfined should be avoided.

In most cases Dispatcher.IO should be used, and in your example the easiest and cleanest solution would be to assign a withContex(Dispatcher.IO) to your function like so:

@GetMapping("/")
suspend fun find(): String = withContext(Dispatchers.IO){
    val start = System.currentTimeMillis()
    println(Thread.currentThread().name + "   starting at " + start)
    val product = async {
        findById()
    }
    val product2 = async {
        findById2()
    }
    println(Thread.currentThread().name + "   ending  " + (System.currentTimeMillis() - start))
    product.await() + product2.await()
}
Related