ForkJoinPool 调度与 ExecutorService

ForkJoinPool scheduling vs ExecutorService

我对 ExecutorServiceForkJoinPool 的内部调度机制有点困惑。

我了解 ExecutorService 调度已完成 this way

一堆任务在排队。一旦线程可用,它将处理第一个可用任务,依此类推。

同时,ForkJoinPool 被显示为不同的,因为它使用了 工作窃取算法 。如果我理解正确的话,这意味着一个线程可以从另一个线程窃取一些任务。

然而,我并不真正理解 ExecutorServiceForkJoinPool 中实现的机制之间的区别。以我的理解,这两种机制都应该尽可能减少每个线程的空闲时间。

如果在 ExecutorService 的情况下,我会理解每个线程都有自己的队列。然而,情况并非如此,因为队列由池中的不同线程共享...

欢迎任何澄清!

假设您有一个非常大的整数数组,并且您想要将它们全部相加。对于 ExecutorService 你可能会说:让我们将该数组分成块,假设线程数 / 4。因此,如果你有一个包含 160 个元素的数组(并且你有 4 个 CPUs),你创建160 / 4 / 4 = 10,因此您将创建 16 个块,每个块包含 10 个整数。创建 runnables/callables 并将其提交给执行程序服务(当然还要想办法在完成后合并这些结果)。

现在您希望 CPU 中的每个人都能承担其中的 4 项任务并进行处理。现在我们再假设一些数字相加起来非常复杂(当然不是,但请耐心等待),结果可能是 3 threads/CPUs 完成了他们的工作,而其中一个只忙于第一个块。当然,没有人希望那样,但有可能发生。现在糟糕的是,你对此无能为力。

ForkJoinPool 所做的是向我提供您希望如何拆分您的任务以及我必须完成的最小工作量的实施,剩下的我会处理。在 Stream API 中,这是用 Spliterators 完成的;主要有两种方法 trySplit (要么 returns null 意味着什么都不能再分割,要么一个新的 Spliterator - 意味着一个新的块)和 forEachRemaning 这将一旦您不能再拆分任务,就处理元素。这就是工作窃取可以帮助你的地方。

你说如何你的块是如何计算的(通常分成两半)以及当你不能再分割时该怎么办。 ForkJoinPool 会将第一个块分派给所有线程,当其中一些线程空闲时 - 它们完成了工作,它们可以从其他线程查询其他队列并查看它们是否有工作。如果他们注意到一些其他线程队列中有块,他们将接受它们,自行拆分它们并处理这些块。甚至可以证明他们并没有自己完成这些块的全部工作 - 一些其他线程现在可以查询该线程的队列并注意到还有工作要做等等......这要好得多现在,当这 3 个线程空闲时,它们可以去做一些其他的工作——而且它们都很忙。


这个例子有点简化,但与实际相差不远。只是你需要比 CPU's/threads 多得多的块才能使工作窃取起作用;因此通常 trySplit 必须有一个智能的实现,并且您需要在流源中包含很多元素。