Home > OS >  Why can `runBlocking` be invoked without providing a CoroutineContext, if there's no default va
Why can `runBlocking` be invoked without providing a CoroutineContext, if there's no default va

Time:02-26

I always check the implementation of the things I use.

Currently I'm using an injection library that doesn't support suspensable functions (Koin), so, only (even if discouraged) for bootstrapping the app, I'm using runBlocking some times.

In order to have richer logs, I'm enriching the coroutine context with some info, yet that info is lost in most context changes (launch, async, and runBlocking among others).

Specially, given the fact that non-suspend methods don't have access to a CoroutineContext, I'm super curious where does runBlocking gets it from.

You can use runBlocking like:

runBlocking {...}

Yet, when I check its implementation, it has two parameters: a CoroutineContext and the suspend block to be executed. None of those parameters have default value, so why can I call it without passing it? I really don't get it!

Additionally, the page says that the default value is EmptyCoroutineContext but the code docs say something about an event loop.

So I ask again? Why can I call it without passing a value, and what's the actual default?

CodePudding user response:

By default runBlocking() starts with an empty coroutine context.

The fact that the context doesn't have a default value is indeed confusing and strange. I think (but I'm not 100% sure) this is because by ctrl clicking on runBlocking() we go the implementation, so actual definition. But the code is compiled against the expect declaration. I didn't find an easy way to see expect declaration directly in IntelliJ, but it can be found here:

public expect fun <T> runBlocking(context: CoroutineContext = EmptyCoroutineContext, block: suspend CoroutineScope.() -> T): T

As we can see, context in this declaration has a default value. Still, this is really confusing when using IntelliJ.

And regarding the mentioned event loop: yes, runBlocking() creates (or re-uses) an event loop, but I don't see how it relates to the coroutine context.

CodePudding user response:

Remember that the context you pass to a coroutine builder like runBlocking or launch is not the context that will actually become available inside the coroutine, but its parent. The builder adds its own items and merges them into the context you provided.

runBlocking uses its own dispatcher, which exists only during the lifetime of the runBlocking function call. You can find this dispatcher in the coroutine context available inside the body of runBlocking, for example using this code:

import kotlinx.coroutines.runBlocking

fun main() {
    runBlocking {
        val ctxElems = coroutineContext.fold(mutableListOf<Pair<Any, Any>>()) { list, element ->
            list.also { it.add(element.key to element) }
        }
        for ((key, value) in ctxElems) {
            println("${key::class.qualifiedName}: $value")
        }
    }
}

This prints

kotlinx.coroutines.CoroutineId.Key: CoroutineId(1)
kotlinx.coroutines.Job.Key: "coroutine#1":BlockingCoroutine{Active}@12843fce
kotlin.coroutines.ContinuationInterceptor.Key: BlockingEventLoop@3dd3bcd

(coroutine dispatchers belong to the broader category of continuation interceptors)

The other part of your question, why you don't have to pass in the seemingly non-default parameter, is answered elsewhere. Basically, it's an artifact of the IDE and the expected vs. actual declarations. The relevant declaration is the expected one, and actual is an implementation detail, but the IDE takes you to that one.

  • Related