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.