Infima
Infima

Reputation: 182

Calling a non-inlined library function with a suspending lambda that could have been inlined

I am calling a non-inlined library function, providing my own function f that needs to suspend because it receives from a channel. When the library function is called I simply want to wait for it to complete in the current context. The library function will always call f within its body (specifically, the library function does pre, f, then post, where f must be called between pre and post, so it could have been an inlined function). However within f the outer coroutine context no longer applies.

My first thought was to surround the suspending call with runBlocking, but this causes a deadlock, because the (potentially single) thread is now blocked until receive completes, which prevents the producer from progressing.

import kotlinx.coroutines.*
import kotlinx.coroutines.channels.*

// Some non-inlined library function that calls given function f immediately with some library provided value
fun libraryFunction(f: (Int) -> Int) =
    f(5)

fun main() {
    // Some outer loop that may sometimes only have 1 thread
    runBlocking(newSingleThreadContext("thread")) {
        // An existing channel
        val channel = produce {
            send("whatwewant")
        }
        // Our code
        libraryFunction { libraryProvidedValue ->
            println(libraryProvidedValue)
            runBlocking {
                println(channel.receive())
            }
            // A return value the library needs
            7
        }
    }
}

What is the best way to solve this issue? Can using the inner runBlocking be prevented?

In other words: is there anyway to pinky-promise that we are, in fact, still within the same coroutine in the lambda function?


As an additional illustration, the following works:

import kotlinx.coroutines.*
import kotlinx.coroutines.channels.*

// Some non-inlined library function that calls given function f immediately with some library provided value
fun libraryFunction(f: (Int) -> Int) =
    f(5)

fun main() {
    // Some outer loop that may sometimes only have 1 thread
    runBlocking(newSingleThreadContext("thread")) {
        // An existing channel
        val channel = produce {
            send("whatwewant")
        }
        // <inline the start of the body of libraryFunction here> (which gives libraryProvidedValue)
        // Our code
        println(libraryProvidedValue)
        println(channel.receive())
        // <inline the end of the body of libraryFunction here>
    }
}

Update: In this case, the only real issue seems to be that the compiler is not aware of the surrounding coroutine being the same in the lambda function body - but the code is entirely possible to run successfully if it did (as can be seen by inlining the library function). In essence, there is nothing wrong with the flow of this code in particular (because the library function calls the lambda function within its body), but it is a shortcoming in the lack of guarantees the compiler can determine. While runBlocking makes the compiler happy, it has unwanted side effects (notably, the nested blocking part, which makes communication with the outside difficult due to the outer runBlocking blocking up the potentially only thread).

Because of this, I decided to rewrite my entire code surrounding this in a style that uses Deferred with an await at the top level, instead of suspend functions. I would regard this as very un-kotlin-ic, and it comes with potential problems of its own (like resource leaks), but it works for my scenario.

Note that this still does not answer the question posed in any way, but I wanted to note it as an alternative decision to make for future users faced with a similar problem.

Upvotes: 1

Views: 410

Answers (1)

Sergio
Sergio

Reputation: 30695

Unfortunately there is only one way to achieve what you want - to use runBlocking, but it has consequences like you described. suspend functions should be called from a coroutine (runBlocking in this case) or another suspend function. So to achieve this without using runBlocking libraryFunction function must accept a suspend f function and be suspend itself:

suspend fun libraryFunction(f: suspend (Int) -> Int) = f(5)

I would suggest first to receive the value from the channel, and then call the libraryFunction:

runBlocking(newSingleThreadContext("thread")) {
    // An existing channel
    val channel = produce {
        send("whatwewant")
    }
    // Our code
    val receivedValue = channel.receive()
    libraryFunction { libraryProvidedValue ->
        println(libraryProvidedValue)
        println(receivedValue)
        // A return value the library needs
        7
    }
}

Upvotes: 1

Related Questions