Potential issues with CoroutineScope and runBlocking semantics
See original GitHub issuerunBlocking
is different from other coroutine builders because it is not an extension of CoroutineScope
.
If there is an outer scope, programmer might mean to carry over elements from its context. It does not do this unless you explicitly pass the context as a parameter.
Example 1
Here’s an example where it leads to confusion:
class App : CoroutineScope {
override val coroutineContext = Job() + Dispatchers.Main
fun dispose() = coroutineContext.cancel()
fun specialFunction() {
runBlocking {
launch {
// programmer intends to wait for this coroutine, which should run on Main thread, to complete.
// Uses runBlocking instead of suspend function.
// But launch does not use outer scope, because runBlocking doesn't do so and overrides it with its own.
}.join()
}
}
}
launch
call in specialFunction
must be replaced with this@App.launch
to fix the issue.
Idiomatic code would be to use suspend
function,
and if that’s not permissible, the programmer should probably do this:
fun specialFunction() {
val job = launch {
// coroutine runs in main thread as expected
}
runBlocking {
job.join()
}
}
Example 2
Assume the programmer has a good reason to carry over the outer context into the runBlocking
call.
For example:
class App : CoroutineScope {
override val coroutineContext = Job() + newSingleThreadContext("MyMainThread")
fun dispose() = coroutineContext.cancel()
fun specialFunction() {
launch {
runBlocking(coroutineContext) {
println("Hello from runBlocking")
}
}
}
}
suspend fun main() {
with(App()) {
specialFunction()
coroutineContext[Job]!!.join()
dispose()
}
}
Here, the programmer passed outer coroutine context as a parameter.
Now, the runBlocking
coroutine will be cancelled when parent is cancelled.
However, this code will never return because there is a deadlock.
From the runBlocking
documentation:
- When [CoroutineDispatcher] is explicitly specified in the [context], then the new coroutine runs in the context of
- the specified dispatcher while the current thread is blocked.
So what if the current thread is the same as the dispatcher’s single thread? You get a deadlock.
I just wanted to share some difficulties I encountered around runBlocking
and CoroutineScope
. Sure, there are ways around these issues, but I think it is worth considering small changes to the behaviour of runBlocking
here.
I think it might make sense to have a version of runBlocking
that is an extension of CoroutineScope
. That way, cases where there is an outer scope, we can have the logic for carrying over its context implemented inside the runBlocking
function. The new version will be prioritized by the compiler when there is a CoroutineScope
, and its context elements are copied as the programmer would expect. I wonder what your thoughts are. I struggle to find great use cases, but I think this edge case is relatively difficult to maneauver as a programmer at the moment.
Issue Analytics
- State:
- Created 5 years ago
- Comments:13 (5 by maintainers)
Top GitHub Comments
cancel
is guaranteed to synchronously execute allinvokeOnCompletion
handlers, so in your code examples there is a guaranteed that it executed beforeonPause
returns. These handlers are explicitly designed to be synchronous: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/invoke-on-completion.htmlDoes it help? Can we close this issue?
I think that it, at least, deserved some kind of an inspection. Maybe we shall warn on any
runBlocking
usage inside classes that implementCoroutineContext
, but we’ll need more use-cases to study what is going on.