Stephan Schröder
05/23/2024, 8:16 PMdoCpuHeavyWork
function is executed 3 times in this example (instead of 2 times):
fun main() {
runBlocking{
val job = launch{
repeat(5) {
yield()
doCpuHeavyWork()
}
}
delay(600.milliseconds)
job.cancel()
}
}
suspend fun doCpuHeavyWork(): Int {
println("I'm doing work!")
var counter = 0
val endTime=System.currentTimeMillis() + 500
while (System.currentTimeMillis()<endTime) {
counter++
}
println("I'm done working!")
return counter
}
I guess, it's an interaction with Kotlin Playground running single-threaded and yield
probably checking if it's CoroutineContext
has been deactivated first and then giving up control to other coroutiens (in this case to the Coroutine canceling the repeat-job), yet the result still feels still borderline buggy!?
You can run the code on Playground: https://pl.kotl.in/l1TetTLSH
Bye the way, you can "fix" the code by adding another yield
after/infornt of the yield
.Sam
05/24/2024, 8:51 AMrunBlocking
is always single-threaded, and indeed I can reproduce this on my machine too. I agree, it's strange! It seems like the third yield()
call is resuming immediately, even though, at that point, the parent coroutine should be eligible to resume instead, having completed its delay()
. I know that yield()
isn't exactly required to be deterministic, but I would still have expected that if any other coroutine is eligible to resume, the dispatcher would prefer that one over the one that called yield()
. 🤔Sam
05/24/2024, 9:22 AMEventLoop
used by runBlocking
maintains a separate delay queue and event queue. On each iteration of the loop, any delayed continuations that have finished their delay get moved from the delay queue to the back of the event queue. So in this case, the sequence of events is:
• delay()
has expired, and is now at the front of the delay queue. This continuation is the one which will call `cancel()`; let's call it task A.
• yield()
adds its own continuation to the event queue. Let's call it task B. It's now the only thing in the event queue. Now it yields control to the dispatcher, as designed.
• Dispatcher checks the delay queue, and moves task A from the front of the delay queue to the back of the event queue. The event queue now contains B, A.
• Dispatcher resumes task B, because it's at the front of the event queue 😬.
The double-yield()
fixes it because it now adds itself to the back of the event queue again, while task A is already in the queue.Sam
05/24/2024, 9:22 AMfun main() = runBlocking {
launch {
delay(1)
println("This should be first")
}
Thread.sleep(1000)
yield()
println("This should be second")
}
Sam
05/24/2024, 9:23 AMrunBlocking
, since other dispatchers don't use the event loop like thatSam
05/24/2024, 9:24 AMSam
05/24/2024, 9:24 AMStephan Schröder
05/24/2024, 10:58 AMSam
05/24/2024, 12:57 PMStephan Schröder
05/24/2024, 1:07 PMSam
05/27/2024, 10:39 AMStephan Schröder
05/28/2024, 7:01 AM