Kotlin Coroutines are super confusing and hard to use

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!

1 Like

You have a point with your example, though the usage of coroutines in your case is not quite orthodox. I played with your example a bit and found that everything works fine if you wrap your code in supervisorScope. In that case couroutine parent does not fail on children failure (in your case the whole runBlocking is failed).

Java itself is already very well designed with threads. Many operations, such as blocking-read-socket, cannot be done without threads. Are coroutines really that useful if threading is not mixed into it?

You should read @elizarov articles on that, but basically, threads and coroutines solve different problems. Threads are mostly done to achieve parallel execution, coroutines are for concurrent access. You can do concurrency with threads and parallelism with coroutines, but the design goals are different. One practical advantage of coroutines is that they are cheap (in terms of resources) for small tasks.

If you are already using CompletableFuture and work stealing pools in Java, the result will be more or less the same with coroutines (but coroutines have much more convenient syntax). If you are still creating new thread for each asynchronous task, coroutines will give much better results.

Take a look here: What Color is Your Function? – journal.stuffwithstuff.com

You can use https://github.com/Kotlin/kotlinx.coroutines/blob/master/reactive/kotlinx-coroutines-reactor/README.md

https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/await-all.html

Use always the GlobalScope to reach the same behavior.

This is a know limitation Support "worker pool" pattern in actor builder and other related operators · Issue #172 · Kotlin/kotlinx.coroutines · GitHub

None can do anything without threads in JVM.
Coroutines are a language feature, I suspect you miss something in your studies (some basics here https://github.com/Kotlin/KEEP/blob/master/proposals/coroutines.md).
Are Reactive Streams really that useful if threading is not mixed into it?
That is, is it possible to use Reactive Streams without a defined threading model? Is it possible to use kotlinx.coroutine without a defined threading model.
The answer is “no” for both: both are only libraries.
Coroutines is a language feature, so many of its behaviour is defined by underliing library.

5 Likes

I’m referring to @alamothe’s comparison to JavaScript. In JS (particularly NodeJS), most calls are asynchronous with callbacks. This is not the case in Java, so there is more need to actually use threads when you call some libraries that do not support coroutines.

This is indeed a problem in the current design of async/await functions. Good writeup of the problem and potential solution is here https://github.com/Kotlin/kotlinx.coroutines/issues/763 You are welcome to join discussion in that issue.

3 Likes