Skip to content
Advertisement

Why Kotlin/Java doesn’t have an option for preemptive scheduler?

Heavy CPU bound task could block the thread and delay other tasks waiting execution. That’s because JVM can’t interrupt running thread and require help from programmer and manual interruption.

So writing CPU bound tasks in Java/Kotlin requires manual intervention to make things run smoothly, like using Sequence in Kotlin in code below.

fun simple(): Sequence<Int> = sequence { // sequence builder
    for (i in 1..3) {
        Thread.sleep(100) // pretend we are computing it
        yield(i) // yield next value
    }
}

fun main() {
    simple().forEach { value -> println(value) } 
}

As far as I understood the reason is that having preemptive scheduler with the ability to interrupt running threads have performance overhead.

But wouldn’t it be better to have a switch, so you can choose? If you would like to run JVM with faster non-preemptive scheduler. Or with slower pre-emtpive (interrupting and switching the tread after N instructions) one but able to run things smoothly and don’t require manual labor to do that?

I wonder why Java/Kotlin doesn’t have such JVM switch that would allow to choose what mode you would like.

Advertisement

Answer

When you program using Kotlin coroutines or Java virtual threads (after Loom), you get preemptive scheduling from the OS.

Following usual practices, tasks that are not blocked (i.e., they need CPU) are multiplexed over real OS threads in the Kotlin default dispatcher or Java ForkJoinPool. Those OS threads are scheduled preemptively by the OS.

Unlike old-style multithreading, however, tasks are not assigned to a thread when they are blocked waiting for I/O. This makes no difference in terms of preemption, since a task that is waiting for I/O couldn’t possibly preempt another running task anyway.

What you don’t get when programming with coroutines, is preemptive scheduling over a large number of tasks simultaneously. If you have many tasks that require the CPU, then the first N will be assigned to a real thread and the OS will time slice them. The remaining ones will wait in the queue until those ones are done.

But in real life, when you have 10000 tasks that need to be simultaneously interactive, they are I/O bound tasks. On average, there aren’t many that require the CPU at any one time, so the number of real threads you get from the default dispatcher or ForkJoinPool is plenty. In normal operation, the queue of tasks waiting for threads is almost always empty.

If you really had a situation where 10000 CPU-bound tasks needed to be simultaneously interactive, well, then you would be sad anyway, because time slicing would not provide a very smooth experience.

Advertisement