Hi everyone. I'd like to get something cleared up ...
# coroutines
d
Hi everyone. I'd like to get something cleared up - whether
withContext()
creates a new coroutine. My understanding has so far been that it allows you to change the context of the currently-running coroutine, without creating a new one. The docs say that you can "... [use] the withContext function to change the context of a coroutine while still staying in the same coroutine". Similarly, Kotlin in Action 2nd Edition says, "To switch dispatchers for an already existing coroutine, you can use the withContext function and pass a different dispatcher" (p.412) The debugging information appears to bear this out as well, with this code:
Copy code
// Run this with JVM option: -Dkotlinx.coroutines.debug

fun log(msg: String) = println("[${Thread.currentThread().name}] $msg")

fun main() {
    runBlocking(Dispatchers.IO) {
        log("Start runBlocking")
        withContext(Dispatchers.Default) { log("Inside withContext") }
        launch { log("Inside launch") }
        async { log("Inside async") }.await()
        log("End")
    }
}
This produces the following output, which also indicates that the code within
withContext()
is running in the same coroutine as
runBlocking()
(note the
@coroutine#
ID numbers on each line)
Copy code
[DefaultDispatcher-worker-1 @coroutine#1] Start runBlocking
[DefaultDispatcher-worker-3 @coroutine#1] Inside withContext
[DefaultDispatcher-worker-2 @coroutine#2] Inside launch
[DefaultDispatcher-worker-3 @coroutine#3] Inside async
[DefaultDispatcher-worker-3 @coroutine#1] End
However, when looking at the source for the
withContext()
function, we can see a
DispatchedCoroutine
is created, and if we print out the actual
coroutineContext
instance in our log:
Copy code
fun log(msg: String) = println("[${Thread.currentThread().name}] $msg")

fun main() {
    runBlocking(Dispatchers.IO) {
        log("Start runBlocking $coroutineContext")
        withContext(Dispatchers.Default) { log("Inside withContext $coroutineContext") }
        launch { log("Inside launch $coroutineContext") }
        async { log("Inside async $coroutineContext") }.await()
        log("End $coroutineContext")
    }
}
We get this output:
Copy code
[DefaultDispatcher-worker-1 @coroutine#1] Start runBlocking [CoroutineId(1), "coroutine#1":BlockingCoroutine{Active}@5335cb3, Dispatchers.IO]
[DefaultDispatcher-worker-2 @coroutine#1] Inside withContext [CoroutineId(1), "coroutine#1":DispatchedCoroutine{Active}@6f1ba73a, Dispatchers.Default]
[DefaultDispatcher-worker-3 @coroutine#2] Inside launch [CoroutineId(2), "coroutine#2":StandaloneCoroutine{Active}@6a356803, Dispatchers.IO]
[DefaultDispatcher-worker-2 @coroutine#3] Inside async [CoroutineId(3), "coroutine#3":DeferredCoroutine{Active}@2ccad1ad, Dispatchers.IO]
[DefaultDispatcher-worker-2 @coroutine#1] End [CoroutineId(1), "coroutine#1":BlockingCoroutine{Active}@5335cb3, Dispatchers.IO]
Here I see that the coroutine ID is preserved inside
withContext()
, but it has a different instance (
DispatchedCoroutine
rather than
BlockingCoroutine
). Also, we can print out the children:
Copy code
fun main() {
    runBlocking(Dispatchers.IO) {
        val mainJob = coroutineContext[Job]!!
        log("Start runBlocking")
        println(mainJob.children.count())
        withContext(Dispatchers.Default) {
            log("Inside withContext")
            println(mainJob.children.count())
        }
        log("End")
        println(mainJob.children.count())
    }
}
This shows that the outer job does have one child while
withContext()
is running.
Copy code
[DefaultDispatcher-worker-1 @coroutine#1] Start runBlocking
0
[DefaultDispatcher-worker-3 @coroutine#1] Inside withContext
1
[DefaultDispatcher-worker-3 @coroutine#1] End
0
So - was my initial understanding incorrect? Or imprecise? Or are we considering
DispatchedCoroutine
to be an "implementation detail"? Or is it just that a new
*Coroutine
(or
Job
) object instance doesn't exactly correspond to a new actual running coroutine? Thanks!
a
(Disclaimer: This is my understanding, I might be wrong here) When we use
withContext()
, the coroutine context changes for the code with in the with context block. Hence, we see a different output for coroutine context.
withContext
starts a new Job that also uses the same existing coroutine.
Copy code
Coroutine1 - Job1 - CoroutineContext1
    |
    |
    withContext() - Coroutine1 - Job2 - CoroutineContext2
d
Thanks @Abhimanyu - I'll keep digging on this a little further!
s
I don't think there is an answer to this question. There's no precise definition for what constitutes a single coroutine, because coroutines are recursively composite in nature. The rule of coroutine transparency means that entering a new coroutine is (at least theoretically) indistinguishable from staying in the same coroutine. To decide whether you're in a new coroutine, you'd need to choose your definition of coroutine, which is just circular.
c
What do you mean by "because coroutines are recursively composite in nature"?
s
That a coroutine can delegate its work to multiple child coroutines, each of which can delegate to its own children, etc., and that this delegation is an encapsulated detail that doesn't stop us from describing the whole chunk of work as "a coroutine"
There are lots of more concrete questions we can answer, like "does it create concurrency" (no) or "does it create a new Job" (yes). But none of those answers the original question, unless we pick one or more of those characteristics and decide that that's our definition of coroutine 🤷