Kotlin 中哪些用例适合Dispatchers.Default?
Which usecases are suitable for Dispatchers.Default in Kotlin?
根据文档,IO
和 Default
调度程序的线程池大小行为如下:
Dispatchers.Default
:默认情况下,此调度程序使用的最大并行度等于 CPU 核心数,但至少为两个。
Dispatchers.IO
:默认为64线程或核心数的限制(取大者)。
除非我遗漏了一条信息,否则在 Default
上执行大量 CPU 密集工作会更有效率(更快),因为 上下文切换会更少经常.
但下面的代码实际上在 Dispatchers.IO
上运行得更快:
fun blockingWork() {
val startTime = System.currentTimeMillis()
while (true) {
Random(System.currentTimeMillis()).nextDouble()
if (System.currentTimeMillis() - startTime > 1000) {
return
}
}
}
fun main() = runBlocking {
val startTime = System.nanoTime()
val jobs = (1..24).map { i ->
launch(Dispatchers.IO) { // <-- Select dispatcher here
println("Start #$i in ${Thread.currentThread().name}")
blockingWork()
println("Finish #$i in ${Thread.currentThread().name}")
}
}
jobs.forEach { it.join() }
println("Finished in ${Duration.of(System.nanoTime() - startTime, ChronoUnit.NANOS)}")
}
我在 8 核 CPU 上有 运行 24 个作业(因此,我可以让 Default
调度程序的所有线程保持忙碌状态)。这是我机器上的结果:
Dispatchers.IO --> Finished in PT1.310262657S
Dispatchers.Default --> Finished in PT3.052800858S
你能告诉我这里缺少什么吗?如果 IO
工作得更好,为什么我应该使用除 IO
以外的任何调度程序(或任何具有大量线程的线程池)。
回答您的问题:Default
调度程序最适合不具有阻塞功能的任务,因为在并发执行此类工作负载时超过最大并行度没有任何好处(the-difference-between-concurrent-and-parallel-execution)。
https://www.cs.uic.edu/~jbell/CourseNotes/OperatingSystems/5_CPU_Scheduling.html
你的实验有缺陷。正如评论中已经提到的,您的 blockingWork
不是 CPU 绑定的,而是 IO 绑定的。一切都与等待有关 - 当您的任务被阻止并且 CPU 无法执行其后续指令时。您的 blockingWork
本质上只是“等待 1000 毫秒”,并行等待 1000 毫秒 X 次比按顺序执行要快。您执行一些计算(生成随机数 - 本质上也可能是 IO 绑定的),但正如已经指出的那样,您的工作人员正在生成或多或少的这些数字,具体取决于底层线程进入睡眠状态的时间。
我进行了一些生成斐波那契数列的简单实验(通常用于模拟 CPU 工作负载)。然而,在考虑了 JVM 中的 JIT 之后,我无法轻易得出任何结果来证明 Default
调度程序的性能更好。可能是上下文切换并不像人们认为的那么重要。可能是调度程序没有为我的工作负载使用 IO 调度程序创建更多线程。可能是我的实验也有缺陷。不能确定 - JVM 上的基准测试本身并不简单,并且将协程(及其线程池)添加到组合中肯定不会使它变得更简单。
但是,我认为这里有更重要的事情需要考虑,那就是阻塞。 Default
调度员对阻塞呼叫更敏感。池中的线程越少,更有可能所有线程都被阻塞,此时没有其他协程可以执行。
您的程序正在线程中运行。如果所有线程都被阻塞,那么你的程序就没有做任何事情。创建新线程是昂贵的(主要是内存方面),因此对于具有阻塞功能的高负载系统来说,这成为一个限制因素。 Kotlin 在引入“暂停”功能方面做得非常出色。您的程序的并发性不再受限于您拥有的线程数。如果一个流需要等待,它只是挂起而不是阻塞线程。然而,“世界并不完美”,并不是所有的东西都“暂停”——仍然有“阻塞”的调用——你有多确定你使用的 no 库在幕后执行这样的调用?拥有权利的同时也被赋予了重大的责任。对于协同程序,需要更加小心死锁,尤其是在使用 Default
调度程序时。事实上,在我看来,IO
dispatcher 应该是默认的。
编辑
TL;DR:您可能真的想创建自己的调度程序。
回过头来看,我的回答有些肤浅。仅通过查看您想要的工作负载类型来决定使用哪个调度程序在技术上是不正确的 运行。将 CPU 绑定的工作负载限制在与 CPU 核心数量相匹配的调度程序中确实优化了吞吐量,但这并不是唯一的性能指标。
事实上,通过仅对所有 CPU 绑定的工作负载使用 Default
,您可能会发现您的应用程序变得无响应!例如,假设我们有一个使用 Default
调度程序的“CPU-bound” long-运行ning 后台进程。现在,如果该进程使 Default
调度程序的线程池饱和,那么您可能会发现开始处理即时用户操作(用户单击或客户端请求)的协程需要等待后台进程先完成!您已经实现了巨大的 CPU 吞吐量,但代价是延迟和应用程序的整体性能实际上有所下降。
Kotlin 不会强制您使用预定义的调度程序。您始终可以为协程的特定任务创建自己的调度程序。
最终是关于:
- 平衡资源。您实际需要多少个线程?您可以负担得起创建多少个线程?它是 CPU 绑定还是 IO 绑定?即使它是 CPU 绑定的,您确定要将所有 CPU 资源分配给您的工作负载吗?
- 分配优先级。了解调度员的工作负载类型 运行。也许某些工作负载需要立即 运行 而其他一些可能需要等待?
- 防止饥饿死锁。确保您当前的 运行ning 协程不会 阻塞 等待正在等待同一调度程序中的空闲线程的协程的结果。
根据文档,IO
和 Default
调度程序的线程池大小行为如下:
Dispatchers.Default
:默认情况下,此调度程序使用的最大并行度等于 CPU 核心数,但至少为两个。Dispatchers.IO
:默认为64线程或核心数的限制(取大者)。
除非我遗漏了一条信息,否则在 Default
上执行大量 CPU 密集工作会更有效率(更快),因为 上下文切换会更少经常.
但下面的代码实际上在 Dispatchers.IO
上运行得更快:
fun blockingWork() {
val startTime = System.currentTimeMillis()
while (true) {
Random(System.currentTimeMillis()).nextDouble()
if (System.currentTimeMillis() - startTime > 1000) {
return
}
}
}
fun main() = runBlocking {
val startTime = System.nanoTime()
val jobs = (1..24).map { i ->
launch(Dispatchers.IO) { // <-- Select dispatcher here
println("Start #$i in ${Thread.currentThread().name}")
blockingWork()
println("Finish #$i in ${Thread.currentThread().name}")
}
}
jobs.forEach { it.join() }
println("Finished in ${Duration.of(System.nanoTime() - startTime, ChronoUnit.NANOS)}")
}
我在 8 核 CPU 上有 运行 24 个作业(因此,我可以让 Default
调度程序的所有线程保持忙碌状态)。这是我机器上的结果:
Dispatchers.IO --> Finished in PT1.310262657S
Dispatchers.Default --> Finished in PT3.052800858S
你能告诉我这里缺少什么吗?如果 IO
工作得更好,为什么我应该使用除 IO
以外的任何调度程序(或任何具有大量线程的线程池)。
回答您的问题:Default
调度程序最适合不具有阻塞功能的任务,因为在并发执行此类工作负载时超过最大并行度没有任何好处(the-difference-between-concurrent-and-parallel-execution)。
https://www.cs.uic.edu/~jbell/CourseNotes/OperatingSystems/5_CPU_Scheduling.html
你的实验有缺陷。正如评论中已经提到的,您的 blockingWork
不是 CPU 绑定的,而是 IO 绑定的。一切都与等待有关 - 当您的任务被阻止并且 CPU 无法执行其后续指令时。您的 blockingWork
本质上只是“等待 1000 毫秒”,并行等待 1000 毫秒 X 次比按顺序执行要快。您执行一些计算(生成随机数 - 本质上也可能是 IO 绑定的),但正如已经指出的那样,您的工作人员正在生成或多或少的这些数字,具体取决于底层线程进入睡眠状态的时间。
我进行了一些生成斐波那契数列的简单实验(通常用于模拟 CPU 工作负载)。然而,在考虑了 JVM 中的 JIT 之后,我无法轻易得出任何结果来证明 Default
调度程序的性能更好。可能是上下文切换并不像人们认为的那么重要。可能是调度程序没有为我的工作负载使用 IO 调度程序创建更多线程。可能是我的实验也有缺陷。不能确定 - JVM 上的基准测试本身并不简单,并且将协程(及其线程池)添加到组合中肯定不会使它变得更简单。
但是,我认为这里有更重要的事情需要考虑,那就是阻塞。 Default
调度员对阻塞呼叫更敏感。池中的线程越少,更有可能所有线程都被阻塞,此时没有其他协程可以执行。
您的程序正在线程中运行。如果所有线程都被阻塞,那么你的程序就没有做任何事情。创建新线程是昂贵的(主要是内存方面),因此对于具有阻塞功能的高负载系统来说,这成为一个限制因素。 Kotlin 在引入“暂停”功能方面做得非常出色。您的程序的并发性不再受限于您拥有的线程数。如果一个流需要等待,它只是挂起而不是阻塞线程。然而,“世界并不完美”,并不是所有的东西都“暂停”——仍然有“阻塞”的调用——你有多确定你使用的 no 库在幕后执行这样的调用?拥有权利的同时也被赋予了重大的责任。对于协同程序,需要更加小心死锁,尤其是在使用 Default
调度程序时。事实上,在我看来,IO
dispatcher 应该是默认的。
编辑
TL;DR:您可能真的想创建自己的调度程序。
回过头来看,我的回答有些肤浅。仅通过查看您想要的工作负载类型来决定使用哪个调度程序在技术上是不正确的 运行。将 CPU 绑定的工作负载限制在与 CPU 核心数量相匹配的调度程序中确实优化了吞吐量,但这并不是唯一的性能指标。
事实上,通过仅对所有 CPU 绑定的工作负载使用 Default
,您可能会发现您的应用程序变得无响应!例如,假设我们有一个使用 Default
调度程序的“CPU-bound” long-运行ning 后台进程。现在,如果该进程使 Default
调度程序的线程池饱和,那么您可能会发现开始处理即时用户操作(用户单击或客户端请求)的协程需要等待后台进程先完成!您已经实现了巨大的 CPU 吞吐量,但代价是延迟和应用程序的整体性能实际上有所下降。
Kotlin 不会强制您使用预定义的调度程序。您始终可以为协程的特定任务创建自己的调度程序。
最终是关于:
- 平衡资源。您实际需要多少个线程?您可以负担得起创建多少个线程?它是 CPU 绑定还是 IO 绑定?即使它是 CPU 绑定的,您确定要将所有 CPU 资源分配给您的工作负载吗?
- 分配优先级。了解调度员的工作负载类型 运行。也许某些工作负载需要立即 运行 而其他一些可能需要等待?
- 防止饥饿死锁。确保您当前的 运行ning 协程不会 阻塞 等待正在等待同一调度程序中的空闲线程的协程的结果。