Suspendable functions in libraries

Hi Kotlin engineers,

We are having a discussion at work whether in our new libraries we should expose suspendable functions from our libraries which have I/O (Rest calls) inside of them or dumb them down into non suspendable ones.

Some folks are against suspendable functions because a few of our older applications (Scala/Java) may want to use these libraries.

I argue that this would make those IO calls blocking, even if we call them in this way:

    val r1 = async(Dispatchers.IO) { //some blocking client }
    val r2 = async(Dispatchers.IO) { //some other blocking client }
    val r3 = async(Dispatchers.IO) { //yet another blocking client }

I think we are still draining the IO pool quickly. Am I correct in this assumption? Is there a workaround to maintain suspendable functionality that works with Java/Scala?

Any thoughts are appreciated.

1 Like

Asynchronous programming (including coroutines, but not limited to them) makes the most sense if we can jump to the asynchronous world and stay there for a longer “time”. Asynchronous code is performant only if interacting with another asynchronous code. If we plan to often switch between synchronous and asynchronous, then that defeats the purpose.

If your function is a bigger process, consisting of multiple concurrent subtasks, switching between CPU calculations and I/O, etc., then I think it could make sense to start it as a whole using runBlocking. If it is merely a single I/O request, then I would assume it doesn’t provide any benefits, but only degrade the performance. For example, if we do runBlocking, inside we switch to Dispatchers.IO and perform a blocking I/O, then instead of simply blocking the caller thread with the I/O, we block the caller thread, perform a context switch to another thread and block it as well. Even if calling the function from Kotlin, we would observe the same limitations.

I think the best approach is to expose suspendable API for Kotlin and a separate API for Java, using something it understands. It could be a blocking API using runBlocking. but even better, we could return futures:

@JvmSynthetic
suspend fun loadData(): String = TODO()

private val scope = CoroutineScope(EmptyCoroutineContext)
internal fun loadDataAsync() = scope.future { loadData() }

We get the best of all worlds:

  • From the coroutine context (Kotlin), we could use it with the maximum performance.
  • From Kotlin outside of coroutines we can call it using standard ways for launching coroutines.
  • From Java in synchronous code we can call get() and it becomes blocking (although, we still waste a one thread)
  • We can also call it asynchronously from Java.

We still need to find a good way to manage threads and background tasks inside the library - your library probably requires some kind of life management. But this is a common thing.

BTW, I used @JvmSynthetic and internal, so the first function is Kotlin-only and the second is Java-only. This is a kind of hack, you can leave both functions visible or you can provide entirely separate interfaces for Kotlin and Java.

3 Likes