question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Potential issues with CoroutineScope and runBlocking semantics

See original GitHub issue

runBlocking 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:closed
  • Created 5 years ago
  • Comments:13 (5 by maintainers)

github_iconTop GitHub Comments

1reaction
elizarovcommented, Apr 23, 2019

I would like to know more details on how this can safely be used. My particular use case is that I need a CoroutineScope[Job]!!.invokeOnCompletion { .. } to be executed before onPause(..) completes.

cancel is guaranteed to synchronously execute all invokeOnCompletion handlers, so in your code examples there is a guaranteed that it executed before onPause returns. These handlers are explicitly designed to be synchronous: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/invoke-on-completion.html

Does it help? Can we close this issue?

1reaction
elizarovcommented, Oct 18, 2018

I think that it, at least, deserved some kind of an inspection. Maybe we shall warn on any runBlocking usage inside classes that implement CoroutineContext, but we’ll need more use-cases to study what is going on.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Coroutines: runBlocking vs. coroutineScope - Baeldung
In this tutorial, we'll compare two methods for launching Kotlin coroutines: runBlocking and coroutineScope. 2. Maven Dependency.
Read more >
runBlocking in Kotlin Coroutines with Example - GeeksforGeeks
Runs a new coroutine and blocks the current thread interruptible until its completion. This function should not be used from a coroutine. It...
Read more >
Coroutines: runBlocking vs coroutineScope - Stack Overflow
The semantics are "concurrent execution that starts immediately when you call launch ", and the rest is up to the internal scheduling. –...
Read more >
Kotlin Coroutines 1.5: GlobalScope Marked as Delicate ...
Possible replacements. In many cases, the use of GlobalScope should be avoided and the containing operation should be marked with suspend ...
Read more >
Kotlin Coroutines are super confusing and hard to use
@Test fun test1() { runBlocking { try { val x = async ... My reply is that mixing coroutines and threads is precisely...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found