May I ask your opinion about the non-suspend API d...
# coroutines
n
May I ask your opinion about the non-suspend API design decision regarding kotlinx-io?
kotlinx-io
... why choose an API as the base of the new library where I/O operations are not
suspend
? 🤔
> ... I believe that the overhead of coroutines was unacceptable for low-level io operations.
I still think that non-suspend I/O operations would be unnatural in Kotlin... (Further details in comment.)
I claim:
• suspend function calls are fast
• non-suspending function calls are very fast
• I/O libraries like JSON parsers, protobuf encoders, HTTP processors etc. are quite sensitive to the cost of making function calls
• Doing one
withContext(<http://Dispatchers.IO|Dispatchers.IO>)
and then doing a bunch of blocking
• calls is likely to be meaningfully faster than doing many suspending calls
Retrofit’s suspend feature works this way. I think both its API and its
performance are great. Doing a suspend call for every byte processed
would harm performance without improving the developer experience.
See https://kotlinlang.slack.com/archives/C0922A726/p1690214615519589?thread_ts=1689261431.918149&amp;cid=C0922A726 and the follow-up replies.
f
On one side I agree that blocking IO may be preferable and less error prone, when doing
Copy code
while (true) {
    readOneByteSuspendThanksToDispatchersIO()
}
can be very inefficient due to the context switch (note: suspending functions are not guaranteed to be invoked from the optimized Dispatchers.Default only. At the same time, doing a blocking a IO on Android ends up being an ANR very easily, and on other platforms there may not be blocking methods (OPFS on browser, I'm looking at you). A possible solution is to have low-level blocking APIs and higher-level (in a separate module) suspending APIs:
Copy code
// kotlinx.io.lowlevel
fun readOneByteFromStream() <- blocking

// kotlinx.io.highlevel
fun readFile(): Flow<BytesChunk> <- suspend
Another option would be to add a RequireOptIn annotation for blocking methods, making the user super aware of the effect of that function and let the user decide when to wrap in a separate dispatcher
Copy code
// kotlinx.io
@Blocking
fun readOneByteFromStream()

@Blocking
fun readFile(processData: (BytesChunk) -> Unit)

// user side
@OptIn(Blocking)
fun myReadFile(): Flow<BytesChunk> = flow {
   readFile { emit(it) }
}.flowOn(<http://Dispatchers.IO|Dispatchers.IO>)
Or even better, have a blocking API with typed coeffect thanks to context receivers
Copy code
// <http://kotlinx.io|kotlinx.io>
context(BlockingIO)
fun readOneByteFromStream()

context(BlockingIO)
fun readFile(processData: (BytesChunk) -> Unit)

fun <R> withBlockingIO(block: BlockingIO.() -> R): R = BlockingIOSingleton.block() 

suspend fun <R> withSuspendingIO(block: BlockingIO.() -> R): R = withContext(<http://Dispatchers.IO|Dispatchers.IO>) { BlockingIOSingleon.block() }

// user side
fun myReadFile(): Flow<BytesChunk> = channelFlow {
   withSuspendingIO {
      readFile { channel.send(it) }
   }
}
This would maybe make happier both parties: a typed coeffect library where the handling (blocking or suspending through a separate dispatcher) can be decided by the user... 🤔
1
👍🏻 1
c
I haven't followed the progress of kotlinx-io, but I probably won't use a non-suspending IO library. Too easy to shoot yourself in the foot.
👍🏻 1
j
An async API does seem to be on the roadmap, but no specifics yet. I think both blocking and async APIs make sense for different use cases.
1
👍🏻 1
🤔 1
l
Looks like they mentioned Project Loom for non-blocking IO calls. What's the play here? Do we stay regular and hope everyone gets the newest JVM, or do we make our own suspend ones?
👍🏻 1