Norbi
01/23/2024, 8:06 AMsuspend
in the end (it is really "viral").
But if I call a non-suspend
library function then all my efforts are lost (because for example in Kotlin/JVM there is no way to call a suspend
function again - runBlocking()
should not be used in this case).
Maybe do you have the same experience?
Of course I'm not talking about example-level programs but really complex, real-world programs.
Wouldn't it be possible to solve it somehow?
I was thinking about compiling all functions as suspend
even it is non-suspend
in the source code. These functions would not allow the usage of any coroutine functionality (like accessing the coroutine context), they would "simply" support suspending and they would pass the continuation to the functions called by them.
Or we could have both suspend
and non-suspend
versions of the same function to support blocking calls as well. I think it would be possible to implement a form of flow-analysis in the compiler which figures out which version of the function should be called (like to invoke the non-suspend
version if the call-chain is originated from blocking platform code).
To summarize, I think coroutines are great, I use them very often but they are much more uncomfortable and complex than programming blocking threads in Java. (And I haven't mention for example debugging problems, lack of runtime metrics, etc.).
What do you think?Sam
01/23/2024, 8:14 AMNorbi
01/23/2024, 8:53 AMchristophsturm
01/23/2024, 9:01 AMchristophsturm
01/23/2024, 9:17 AMDmitry Khalanskiy [JB]
01/23/2024, 9:51 AMsuspend
functions don't pollute everything because the tasks they do are typically self-contained. For example, "download a file from the network" or "compute a complex thing." There, you'd have the pattern like
class FileDownloader(val scope = CoroutineScope(<http://Dispatchers.IO|Dispatchers.IO>)) {
fun downloadFile(url: URL): Deferred<HTML> = scope.async { /* ... */ }
}
As you can see, downloadFile
is not a suspend
function and can be called anywhere.
downloadFile
would be called in something like
launch {
val file = downloader.downloadFile()
ui.showSpinner()
ui.onCancel { file.cancel() }
val contents = file.await()
ui.displayFile(contents)
}
Basically, you make non-suspend
"commands" to start some async process, and then, in the async contexts, you await the results of these processes.Norbi
01/23/2024, 10:02 AMIt's tough to comment on this without knowing the domain in which you work.Sorry for leaving out this important context: I mainly develop on the server side, for JVM. But I also have a "toy" open source library project where I try to make everything more general than in my JVM-only applications. I use Compose HTML for the UI but I don't have much trouble with "function coloring" there (maybe because Compose was developed with coroutines in mind from the start; and maybe because coroutines are a natural extension of the single-threaded JS environment IMHO).
Dmitry Khalanskiy [JB]
01/23/2024, 10:11 AMFileDownloader
) is typically applicable to the server-side Kotlin code, but if you have a specific long chain of functions all of which you had to mark as suspend
and don't know how to decompose, please share it, and maybe we'll work something out.Norbi
01/25/2024, 10:20 AMsuspend fun fetchToken() { ... }
...
HttpClient().get("...") {
headers {
append(HttpHeaders.Authorization, "Bearer ${fetchToken()}")
}
}
where the library author thought that only blocking calls will be needed at a given place.
Of course I can work around these in almost every case (it is especially easy with the above example) but it usually makes the code more complicated.Dmitry Khalanskiy [JB]
02/01/2024, 11:56 AMHttpClient
is outside your control, looks like just runBlocking { fetchToken() }
should do the trick, no?Norbi
02/01/2024, 1:12 PMjustNo. As I know you cannot useshould do the trick, norunBlocking { fetchToken() }
runBlocking()
from a "coroutine", and get()
is running in a coroutine because it is suspend
.
A huge problem with runBlocking()
is that you cannot be sure if it can be safely used unless you have full control of the current "function call chain".Dmitry Khalanskiy [JB]
02/01/2024, 1:14 PMget
is suspend
, but the code in the lambda isn't?Norbi
02/01/2024, 1:19 PMsuspend
.
https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/run-blocking.html
... This function should not be used from a coroutine. It is designed to bridge regular blocking code to libraries that are written in suspending style, to be used inAt least this is my understanding offunctions and in tests.main
runBlocking()
- please correct me if I was wrong for years :)Dmitry Khalanskiy [JB]
02/01/2024, 1:25 PMsuspend fun f() {
runBlocking { g() }
}
That's definitely a mistake we've seen many people make.
What runBlocking
does is, essentially, block the thread until its lambda completes. In theory, this could lead to deadlocks. For example, if you have a thread pool with just two threads and you do something like this:
val result = CompletableDeferred<Int>()
val task1 = launch(pool) {
runBlocking {
result.await()
}
}
val task2 = launch(pool) {
runBlocking {
result.await()
}
}
val task3 = launch(pool) {
result.complete(3)
}
Here, both task1
and task2
block a thread from the thread pool, so task3
has no chance to run.Dmitry Khalanskiy [JB]
02/01/2024, 1:26 PMrunBlocking
exactly: you intend to call suspend
code in a non-suspend
context and, because of the way get
is written, have no way of obtaining a suspend
context.Dmitry Khalanskiy [JB]
02/01/2024, 1:33 PMsuspend fun fetchToken() { ... }
...
val future = CompletableFuture<Token>()
launch { future.complete(fetchToken()) }
HttpClient().get("...") {
headers {
append(HttpHeaders.Authorization, "Bearer ${future.get()}")
}
}
then you get exactly the same risk of a deadlock, but arguably with less clarity. Blocking calls inside coroutines are like that.Norbi
02/01/2024, 1:38 PMrunBlocking()
.Dmitry Khalanskiy [JB]
02/01/2024, 1:40 PMrunBlocking
and try to find specific places that caused confusion?Norbi
02/01/2024, 1:59 PMfun someLibraryFunction(block: () -> Unit) = block()
suspend fun myFunction1() = ...
suspend fun myFunction2() = someLibraryFunction { runBlocking { myFunction1() } }
But based on your above comments this is valid and supported usage, although it obviously blocks the current thread until runBlocking()
returns, so it should be used with great care. (But I can use withContext(<http://Dispatchers.IO|Dispatchers.IO>) { someLibraryFunction { runBlocking { myFunction1() } } }
to avoid most problems I think).
Thanks for the clarification.