Skip to content

Nested runBlocking deadlocks when a task depends on the nested runBlocking itself #3982

Open
@dovchinnikov

Description

@dovchinnikov

What do we have now?

The following deadlocks because nested runBlocking tries to process tasks from the outer runBlocking, and one of the tasks actually depends on the completion of the nested runBlocking:

import kotlinx.coroutines.*

class Container(private val scope: CoroutineScope) {

  private val instances = HashMap<Class<*>, Deferred<Any>>() // basically a map

  suspend fun <T> instance(key: Class<out T>): T {
    return instances
      .computeIfAbsent(key, ::instanceDeferred)
      .await() as T
  }

  private fun instanceDeferred(clazz: Class<*>): Deferred<Any> = scope.async {
    val constructor = clazz.getConstructor(Container::class.java)
    constructor.isAccessible = true
    constructor.newInstance(this@Container)
  }
}

class MyService(container: Container) {
  init {
    runBlocking {
      // this can ask container for an unrelated service,
      // or in this case it's enough for it to be empty
    }
  }
}

class MyService2(container: Container) {
  val service1 = runBlocking {
    container.instance(MyService::class.java)
  }
}

fun main() {
  runBlocking {
    val container = Container(this@runBlocking)
    launch {
      println(container.instance(MyService::class.java))
    }
    launch {
      println(container.instance(MyService2::class.java).service1)
    }
  }
}

What should be instead?

runBlocking should not steal tasks from outer runBlocking.

Alternatively, please provide an API to make runBlocking not steal tasks. It may be a boolean, or a special coroutine context element. Currently we have to use internals.

Why?

Because otherwise it deadlocks.

Notes

Originally, the "stealing" was added to fix a very specific use-case #860.

I believe that if nested runBlocking "steals" tasks while being inside a limited dispatcher, then this stealing logic should apply to all limited dispatchers, i.e. runBlocking on a thread inside Dispatchers.Default should steal tasks from Dispatchers.Default (#3439). If that's not possible for any reason, then nested runBlocking should not steal tasks from the outer runBlocking for the very same reason whatever it might be.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions