Description
fun main() = runBlocking {
launch {
delay(1)
println("This should be first")
}
Thread.sleep(1000)
yield()
println("This should be second")
}
Actual behaviour
The code outputs:
This should be second
This should be first
The call to yield()
resumes immediately, instead of yielding control to the other coroutine.
This seems to be an issue with the EventLoop
dispatcher used by runBlocking
. It maintains a separate queue for delayed continuations. On each iteration of the event loop, tasks whose delay has expired are moved from the delay queue to the main event queue. During a call to yield()
, this takes place after the yield()
function has added its own continuation to the event queue. That means a yield()
call will always fail to yield if the only other continuations waiting to resume are ones whose delay has expired since the last dispatch.
Expected behaviour
Output should be:
This should be first
This should be second
On reaching the call to yield()
, the launch
job is already eligible to resume. 1 second has elapsed, which is much longer than the 1ms required delay. The dispatcher should prefer to dispatch the launch
job, instead of resuming the same coroutine that called yield()
.
Environment
Reproduced using Kotlin 1.9 with coroutines 1.8.1, on JVM 1.8.
This issue was first reported by a user on Slack: https://kotlinlang.slack.com/archives/C0B8MA7FA/p1716495410746749