为什么 Kotlin/Java 没有抢占式调度程序的选项?

Ale*_*hin 5 java multithreading jvm kotlin

重 CPU 绑定任务可能会阻塞线程并延迟其他等待执行的任务。那是因为JVM不能中断正在运行的线程,需要程序员的帮助和手动中断。

因此,在 Java/Kotlin 中编写 CPU 绑定任务需要手动干预才能使事情顺利运行,就像Sequence在下面的代码中在 Kotlin 中使用一样。

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) } 
}
Run Code Online (Sandbox Code Playgroud)

据我了解,原因是具有能够中断正在运行的线程的抢占式调度程序具有性能开销。

但是有一个开关不是更好,所以你可以选择吗?如果您想使用更快的非抢占式调度程序运行 JVM。或者使用较慢的 pre-emtpive(在 N 条指令后中断和切换胎面)但能够平稳运行并且不需要体力劳动来做到这一点?

我想知道为什么 Java/Kotlin 没有这样的 JVM 开关来允许选择你想要的模式。

Jon*_*oni 5

这个问题基于一个错误的前提:在 JVM 中,抢占式调度程序是您唯一的选择。没有现代 JVM 使用协作式多任务处理。

没有现代 JVM 实现用户空间线程或自己的调度程序。JVM 使用本机操作系统线程来代替。本机线程由操作系统调度,操作系统调度程序抢占式的。

JVM 线程一对一映射到本机操作系统线程这一事实对于需要高并发性的应用程序来说是一个问题。线程相对稀缺且昂贵。为了解决这个问题,Loom 项目正在研究添加“虚拟线程”,这可能允许更谨慎地使用本机线程,尤其是对于 I/O 绑定任务。

Project Loom 正在积极开发中,何时成为标准 Java 的一部分还没有确定的时间表。关于 Project Loom 如何调度“虚拟线程”,来自 Project Loom的最新(2020 年 5 月)更新声称“虚拟线程是抢占式的,而不是协作式的”,但接着说“JDK 中目前没有任何调度程序使用时间片-基于虚拟线程的抢占”。听起来在当前状态下,Project Loom 中的“虚拟线程”调度程序介于完全合作和完全先发制人之间。看看这个项目是如何发展的,以及当它被集成到主流 Java 时我们会得到什么,将会很有趣。

7 月 28 日的问答中,Loom 项目负责人 Ron Pressler提到您将能够为虚拟线程插入自己的调度程序,但没有详细说明您对调度算法的控制程度。

  • @AlexeyPetrushin 这是不正确的。当每个线程的时间片结束时,操作系统调度程序会中断每个线程,并评估是否需要切换到另一个任务。 (3认同)
  • @AlexeyPetrushin 为什么你说你必须编写更少的代码?您是否错过了“操作系统调度程序是抢占式的”这一事实? (2认同)

Mat*_*ans 5

当您使用 Kotlin 协程或 Java 虚拟线程(在 Loom 之后)进行编程时,您可以从操作系统获得抢占式调度。

按照通常的做法,未被阻塞的任务(即,它们需要 CPU)在 Kotlin 默认调度程序或 Java ForkJoinPool 中的真实操作系统线程上多路复用。这些操作系统线程由操作系统抢占式调度。

然而,与旧式多线程不同,当任务被阻塞等待 I/O 时,不会将任务分配给线程。这在抢占方面没有区别,因为等待 I/O 的任务无论如何都不可能抢占另一个正在运行的任务。

使用协程编程时,您不会得到的是同时对大量任务进行抢占式调度。如果您有许多需要 CPU 的任务,那么前 N 个将分配给一个真正的线程,操作系统将对它们进行时间切片。其余的将在队列中等待,直到这些完成。

但在现实生活中,当你有 10000 个任务需要同时交互时,它们就是 I/O 绑定任务。平均而言,任何时候都没有多少需要 CPU 的,因此您从默认调度程序或 ForkJoinPool 获得的实际线程数量很多。 在正常操作中,等待线程的任务队列几乎总是空的。

如果您真的遇到需要同时交互10000 个CPU 密集型任务的情况,那么无论如何您都会感到难过,因为时间切片不会提供非常流畅的体验。