Hi,
I posted this question earlier but got no response. What do you think is the output of the following code:
@Test
fun test1() {
runBlocking {
try {
val x = async {
delay(1000)
throw Exception()
10
}
println("Awaiting...")
x.await()
} catch (e: Exception) {
println("Here")
delay(1000)
println("What?!")
}
}
}
Without deep understanding of coroutine scopes etc. you will guess wrong.
I don’t have all the context that went into designing coroutines, like research and other languages that served as an inspiration. But I feel it ended up being over-engineered, low-level and very hard to understand from the userland. In general, users only need two paradigms 99% of the time:
- Reactor pattern for the backend
- UI thread pattern for some clients
It will be a good case study and a learning experience. I believe where it went south is not choosing the right primitives. If the primitives were right, then concepts such as coroutines scope would be implicit. For example, I think it’s wrong that awaiting for one deferred will start all other jobs in the same scope.
Can coroutines still be fixed? I think so, by providing better higher-level primitives. For example, something like Promise.all
in JavaScript with precisely defined semantics. Why doesn’t it exist in Kotlin?
Small digression: JavaScript with it’s simple primitives (promises, async
, await
, Promise.all
) is 10x easier to understand than Kotlin coroutines, and beats them hands down. You can say that JavaScript doesn’t allow multi-threading, and that’s why it’s simpler. My reply is that mixing coroutines and threads is precisely the problem. Multi-threading is inherently hard (but fortunately rarely needed in practice), coroutines should and can be easy (and in practice solve 99% of cases where parallelism is needed - the other 1% should be left to multi-threading).
To contribute something here, this is what I end up using 99% in the backend:
suspend fun <T, R> Iterable<T>.mapAsync(transform: suspend (T) -> R): List<R> {
return coroutineScope {
val list = this@mapAsync.map { e ->
async {
transform(e)
}
// Must not use await here, all coroutines should be created first
}
list.map { it.await() }
}
}
It’s same as map
, but allows for suspending transform, and will run them in parallel.
Any comments are welcome!