这是今天对另一个问题的答案的“副作用” 。它更多的是关于好奇心而不是实际问题。
Java SE 7 提供了 Oracle 所称的“fork/join 框架”。这可能是一种将工作安排到多个处理器的优越方式。虽然我了解它应该如何工作,但我无法理解它的优越之处以及关于窃取工作的说法。
也许其他人更了解为什么这种方法是可取的(除了因为它有一个花哨的名字)。
fork/join 的底层原语是ForkJoinTask
s,即Future
s,其想法是要么立即执行工作 [原文如此](措辞具有误导性,因为“立即”意味着它在主线程中同步发生,实际上这发生在内部a Future
) 低于某个阈值或递归地将工作分成两个任务,直到达到阈值。
未来是将异步运行的任务以不透明和未指定的方式封装到对象中的概念。您有一个函数可以让您验证结果是否可用,并且您有一个函数可以让您(等待和)检索结果。
严格来说,你甚至不知道未来是否异步运行,它可以在内部执行get()
。该实现在理论上也可以为每个未来生成一个线程或使用一个线程池。
在实践中,Java 将 future 作为任务队列上的任务实现,并附加一个线程池(整个 fork/join 框架也是如此)。
fork/join 文档给出了这个具体的使用示例:
protected void compute() {
if (mLength < sThreshold) {
computeDirectly();
return;
}
int split = mLength / 2;
invokeAll(new ForkBlur(mSource, mStart, split, mDestination),
new ForkBlur(mSource, mStart + split, mLength - split,
mDestination));
}
这以与 Mergesort 遍历它们的方式相同的方式将任务提交到底层线程池的任务队列(感谢递归)。
例如,我们有一个包含 32 个“项目”的数组要处理,阈值为 4,然后平均拆分,它将产生 8 个任务,每个任务有 4 个“项目”,如下所示:
00 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
.
00 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15|16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
. . .
00 01 02 03 04 05 06 07|08 09 10 11 12 13 14 15|16 17 18 19 20 21 22 23|24 25 26 27 28 29 30 31
. . . . . . .
00 01 02 03|04 05 06 07|08 09 10 11|12 13 14 15|16 17 18 19|20 21 22 23|24 25 26 27|28 29 30 31
------------------------------------------------------------------------------------------------
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
在单核处理器上,这将按顺序提交/执行(以非常复杂的方式)任务组 1-2-3-4-5-6-7-8。
在双核处理器上,这将提交/执行 (1,3)-(2,4)-(5,7)-(6,8) [1]。
在四核处理器上,这将提交/执行 (1,3,5,7)-(2,4,6,8)。
相比之下,没有所有高级魔法的幼稚实现只会立即将任务 1-2-3-4-5-6-7-8 提交到任务队列。总是。
在单核处理器上,这将提交/执行 1-2-3-4-5-6-7-8。
在双核处理器上,这将提交/执行 (1,2)-(3,4)-(5,6)-(7,8)。
在四核处理器上,这将提交/执行 (1,2,3,4)-(5,6,7,8)。
问题:
不是简单地将sThreshold个连续的项目塞进一个任务中,然后一个接一个地提交到线程池的任务队列中,而是生成了一个树状的递归层次结构。这涉及为实际上什么都不做的 N 个子任务构建、引用和销毁 N+log2(N) 个对象。为什么这个优越?
不保留参考位置。处理器缓存和虚拟内存都不会被这样对待。为什么这个优越?
除了在单处理器系统上,任务保证不会以接近其原始顺序的顺序进行调度。如果它真的无关紧要,这可能没有问题,但它使诸如栅栏或屏障之类的东西几乎不可行。拥有类似栅栏的唯一方法是等待根对象完成,然后才提交新任务。这相当于一个完整的管道停顿(这正是您不希望发生的事情)。
Oracle 文档声称这种方法实现了工作窃取,因此比线程池更好。我没有看到这种情况发生。我所看到的只是将任务提交到普通线程池的一种非常复杂的方式。这应该如何神奇地实现工作窃取?
[1]让我们不要把它弄得太复杂,并假设工作线程不会相互超越,任务都需要相同的时间来处理。否则,执行当然可能以不同的顺序发生,尽管提交是相同的。