I am facing an issue with a Kotlin coroutine. When I run the suspend function performTask
I notice that the log message "Step 1: Task within withContext" is printed first, and only after 10 seconds, the message "Step 2: Task outside withContext" is printed.
Here's the code:
suspend fun weirdFunction() {
CoroutineScope(coroutineContext).launch {
delay(10_000)
}
}
suspend fun performTask() {
withContext(Dispatchers.Default) {
weirdFunction()
Log.d("MyTag", "Step 1: Task within withContext")
}
Log.d("MyTag", "Step 2: Task outside withContext")
}
I believe the issue is caused by using CoroutineScope(coroutineContext).launch
inside weirdFunction
, but I don't understand why this usage leads to this behavior. My intention was to launch a child coroutine within the current coroutine scope without passing the scope as a parameter. I thought using coroutineContext
to obtain the current coroutine scope was an elegant way to achieve this.
However, it seems my understanding might be incorrect. This issue made me think about a more concerning scenario: if weirdFunction
were part of a third-party library, it could cause withContext
to hang and not return as expected, which would make debugging very difficult.
Could someone explain why this approach causes the withContext
block to wait for the launched coroutine to complete? Is there a better way to launch a non-blocking coroutine within a suspend function that doesn't affect the caller's coroutine scope? Any insights or explanations would be greatly appreciated.
The following happens when you run performTask
:
withContext
creates a new job to run block
synchronously and waits for it to finish. Let's call this job job1. It doesn't matter what you pass for the parameter of withContext
.weirdFunction
is called and a new coroutine scope is created. You always need to associate a coroutine scope with a job. If you don't provide one a new one is created under the hood. In this case, however, you did provide a job, because job1 is part of the coroutineContext
.weirdFunction
returns and "Step 1: Task within withContext" is logged.withContext
block is finished... almost! job1 that was used to run the block is still running, because one of its children is still running: job2 is not finished yet, it ist still waiting for 10 seconds. So job1 is also running and withContext
will only return when that is finished.withContext
returns and "Step 2: Task outside withContext" is logged.If you hadn't attached job1 to the new coroutine scope you would have achieved the desired behavior:
CoroutineScope(Job()).launch {
delay(10_000)
}
However, suspend functions shouldn't try to launch new coroutines that outlive the function itself. The reason for that is that you won't have a way to control that coroutine, for example, to cancel it when it isn't needed anymore.
If you need to launch a new coroutine you should pass a CoroutineScope
instance as a parameter. Inside the suspend function it can be used to launch a new coroutine, outside the suspend function it can be used to cancel the coroutine. You can also specify the parameter as the receiver of an extension function, like this:
fun CoroutineScope.weirdFunction() {
launch {
delay(10_000)
}
}
Please note that this isn't even a suspend function anymore because everything coroutine-related is applied to the coroutine scope.
This makes the behavior of the function more transparent: It isn't a suspend function so you don't need a coroutine to call it, but you do need to provide a coroutine scope.
The expectation of a suspend function on the other hand is that it performs what it needs to do; it may suspend in between, but when that function returns it also finished what it set out to do and there is no dangling coroutine left that you wouldn't have any control over.
When you call this new weirdFunction
from withContext
it would exhibit the same behavior as before, but this is expected from just looking at the function signature because you know that you pass the current coroutine scope with the withContext
's context (and its job), so whatever happens inside weirdFunction
it is clear that withContext
will wait for it to finish.
When this is not the desired behavior you can always provide another coroutine scope that is not dependent on withContext
:
scope.weirdFunction()
Just make sure to keep a reference to that scope around so you cancel it when it isn't needed anymore.