简体   繁体   中英

Kotlin: How to wait for a coroutine from non-suspend without runBlocking?

Edit 2: I think I misunderstood the documentation. I read:

runBlocking

This function should not be used from a coroutine. It is designed to bridge regular blocking code to libraries that are written in suspending style, to be used in main functions and in tests.

To mean that I shouldn't use runBlocking() at all other than for main or tests. But I now realise I read it wrong, specifically this part:

It is designed to bridge regular blocking code to libraries that are written in suspending style

So it seems that runBlocking should be used in this scenario.

However I think I should fully grok the topic of contexts to see which contexts is best suited to pass to runBlocking in this case:

return runBlocking(???????) {
    job1.await() + job2.await()
}

Edit: Clearly I phrased the question badly as all attempts to answer it miss the actual question and the constraint I am posing. So let's try a different approach...

This works:

fun doSomething(): Int {
    val job1 = GlobalScope.async { calculateSomething() }
    val job2 = GlobalScope.async { calculateSomething() }
    return runBlocking {
        job1.await() + job2.await()
    }
}

suspend fun calculateSomething(): Int {
    delay(1000L)
    return 13
}

suspend fun calculateSomethingElse(): Int {
    delay(2000L)
    return 19
}

My question is: is there anyway I can achieve the same result:

  1. Without using runBlocking() at all.
  2. Without turning doSomething() into a suspend function.

?

In other words: is there anything I can put instead of ?????? to make the following work?

fun doSomething(): Int {
    val job1 = GlobalScope.async { calculateSomething() }
    val job2 = GlobalScope.async { calculateSomething() }
    return ????????? {
        job1.?????() + job2.?????()
    }
}

suspend fun calculateSomething(): Int {
    delay(1000L)
    return 13
}

suspend fun calculateSomethingElse(): Int {
    delay(2000L)
    return 19
}

I have a small utility method that runs any given external command and returns its output (ie small wrapper around Java Process API):

 
 
 
 
  
  
  class RunningCommand(private val proc: Process) { fun waitFor(): String { proc.outputStream.close() var output = "" val outputRdr = thread { output = proc.inputStream.bufferedReader().use { it.readText() } } var error = "" val errorRdr = thread { error = proc.errorStream.bufferedReader().use { it.readText() } } proc.waitFor() outputRdr.join() errorRdr.join() if (proc.exitValue() != 0) { throw RuntimeException("Command returned non-zero status: $output$error") } return output } }
 
 
 

This code works correctly. However it creates two additional threads for each command execution. I wanted to avoid that by switching to coroutines. I was able to do this, but I had to use runBlocking :

 
 
 
 
  
  
  class RunningCommand(private val proc: Process) { fun waitFor(): String { proc.outputStream.close() var output = "" val outputRdr = GlobalScope.async { output = proc.inputStream.bufferedReader().use { it.readText() } } var error = "" val errorRdr = GlobalScope.async { error = proc.errorStream.bufferedReader().use { it.readText() } } proc.waitFor() runBlocking { outputRdr.await() errorRdr.await() } if (proc.exitValue() != 0) { throw RuntimeException("Command returned non-zero status: $output${error}") } return output } }
 
 
 

This code also works, but I read that runBlocking should only be used on main() methods and tests, ie not meant to be used in this way. Peeking at its implementation it looks horrible and indeed looks like something one wouldn't want to call repeatedly from some utility method.

So my question is: how else am I supposed to bridge the gap between blocking code and coroutines? Or put differently, what is the correct way to wait for a suspend function from non- suspend code?

Or is it simply that my design is wrong, and to use Coroutines anywhere down the line I need to make the main() method runBlocking and essentially always be inside some coroutine scope?

For any future travellers who make the same mistake as me - runBlocking is OK to use not just in main / tests - but also:

It is designed to bridge regular blocking code to libraries that are written in suspending style

Somehow I got the impression that it is evil to use for just some library function, but it is not.

You can create your own scope with dispatchers that do operations in the background. You can use withContext if you want to wait for something to completely finish executing.

private val myScope = CoroutineScope(Dispatchers.Main) 

myScope.launch {

   withContext(Dispatchers.IO) {
        //to stuff in the background
    }

}

You run the code bellow, you'll see that it prints 20, not null.

fun main() {
    callSuspendFun()
}

suspend fun doWorkAndReturnDouble(num: Int): Int {
    delay(1000)
    return num * 2
}

fun callSuspendFun() {
    val coroutineScope = CoroutineScope(Dispatchers.Main)
    coroutineScope.launch {
        var num: Int? = null
        withContext(Dispatchers.IO) {
            val newNum = 10
            num = doWorkAndReturnDouble(newNum)
        }
        println(num)
    }
}

So to call a suspend function from a non-suspend function, without using runBlocking, you have to create a coroutine scope. And withContext you wait for the execution of the code.

You should use coroutineScope

suspend fun waitFor(): String = coroutineScope {
    proc.outputStream.close()

    var output = ""
    val outputRdr = async { output = proc.inputStream.bufferedReader().use { it.readText() } }
    var error = ""
    val errorRdr = async { error = proc.errorStream.bufferedReader().use { it.readText() } }

    proc.waitFor()

        outputRdr.await()
        errorRdr.await()

    if (proc.exitValue() != 0) {
        throw RuntimeException("Command returned non-zero status: $output${error}")
    }

    return output
}

You can use CoroutineScope() with Dispathers.IO which will launch a coroutine in background thread and would offload your execution on that thread

class RunningCommand(private val proc: Process) {

fun waitFor(): String {
    // Starting a new coroutine on Background thread (IO)
    proc.outputStream.close()
    var output = ""
    CoroutineScope(Dispatchers.Unconfined).async {

        val outputRdr = async { output = proc.inputStream.bufferedReader().use { it.readText() } }
        var error = ""
        val errorRdr = async { error = proc.errorStream.bufferedReader().use { it.readText() } }

        proc.waitFor()

        // No need of runBlocking 
        // await() call would wait for completion of its async block
        outputRdr.await() // Execution would block until outputRdr block completion on IO thread
        // At this stage, outputRdr block is completed
        errorRdr.await() // Execution would block until errorRdr block completion on IO thread
        // At this stage, errorRdr block is completed


        if (proc.exitValue() != 0) {
            throw RuntimeException("Command returned non-zero status: $output${error}")
        }
    return@async output
    }
    return output
}

}

Note : If you're calling waitFor() method from any coroutine context, you can carry on work on same coroutine context by writing coroutineScope { } instead of CoroutineScope(Dispatchers.IO).launch { } , managing structured concurrency properly.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM