我对ExecutorService
和ForkJoinPool
的内部调度机制有点困惑。
我知道ExecutorService
调度是这样完成的。
一堆任务排队。一旦线程可用,它将处理第一个可用任务,依此类推。
同时,ForkJoinPool
被呈现为不同的,因为它使用工作窃取算法。如果我理解正确,这意味着一个线程可以从另一个线程窃取一些任务。
然而,我真的不明白在ExecutorService
和ForkJoinPool
中实施的机制之间的区别。据我了解,这两种机制都应该尽可能减少每个线程的空闲时间。
我会理解在ExecutorService
的情况下,每个线程都有自己的队列。然而,事实并非如此,因为队列由池的不同线程共享......
任何澄清都将非常受欢迎!
假设您有一个非常大的整数数组,并且想要添加所有整数。用一个ExecutorService
你可能会说:让我们把这个数组分成几个块,比如说线程数/4。因此,如果你有一个包含 160 个元素的数组(并且你有 4 个 CPU(,你就会创建160 / 4 / 4 = 10
,所以你会创建 16 个块,每个块保存 10 个整数。创建可运行对象/可调用对象并将其提交给执行器服务(当然,一旦完成,还要想出一种方法来合并这些结果(。
现在,您的希望是每个CPU将承担其中的4个任务并处理它们。现在让我们还假设一些数字的添加非常复杂(当然不是,但请耐心等待(,结果可能是 3 个线程/CPU 完成了它们的工作,而其中一个线程/CPU 只忙于第一个块。当然,没有人希望这样,但可能会发生。现在的坏事是你对此无能为力。
相反,ForkJoinPool
所做的是告诉我您希望如何拆分任务以及我必须做的最小工作量的实现,其余的我会负责。在Stream API
中,这是用Spliterator
s完成的;主要使用两种方法trySplit
(要么返回null
意味着什么都不能再拆分,要么是一个新的Spliterator
- 意味着一个新的块(和forEachRemaning
,一旦你不能再拆分你的任务,它将处理元素。这就是偷工作对你有帮助的地方。
你说你的块是如何计算的(通常分成两半(,以及当你不能再分裂时该怎么办。ForkJoinPool
会将第一个块分派给所有线程,当其中一些线程空闲时 - 他们完成了他们的工作,他们可以从其他线程查询其他队列并查看他们是否有工作。如果他们注意到其他线程队列中有块,他们会获取它们,自行拆分它们并处理这些块。甚至可能证明他们不会自己完成该块的整个工作 - 其他一些线程现在可以查询此线程的队列并注意到仍有工作要做等等......这比现在好得多,当这 3 个线程空闲时,他们可以挑选一些其他工作来做 - 而且他们都很忙。
这个例子有点简化,但离现实不是很远。只是你需要比 CPU/线程更多的块才能窃取工作;因此,通常trySplit
必须有一个智能实现,并且流的源中需要大量元素。