Java fork/join 框架逻辑
这是今天另一个问题的答案的“副作用”。这更多的是关于好奇心,而不是一个实际的问题。
Java SE 7提供了Oracle所谓的“分叉/连接框架”。这可能是将工作安排到多个处理器的一种更好的方法。虽然我理解它应该如何工作,但我不明白它优越的地方以及关于工作盗窃的说法。
也许其他人对为什么这种方法是可取的有更多的见解(除了因为它有一个花哨的名字)。
fork/join的基础原语是s,它们是s,其思想是立即执行工作[原文如此](措辞具有误导性,因为“立即”意味着它在主线程中同步发生,实际上这发生在低于某个阈值的a)内,或者将工作递归地分成两个任务,直到达到阈值。ForkJoinTask
Future
Future
未来是将以不透明且未指定的方式异步运行的任务封装到对象中的概念。您有一个函数,用于验证结果是否可用,并得到一个函数,允许您(等待和)检索结果。
严格来说,您甚至不知道将来是否异步运行,它可以在 内部执行 。从理论上讲,该实现还可以为每个将来生成一个线程或使用线程池。
在实践中,Java将未来实现为任务队列上的任务,并附加了线程池(整个fork/join框架也是如此)。get()
分叉/连接文档给出了以下具体用法示例:
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] 我们不要让它变得太复杂,并假设工作线程不会相互运行,任务都需要相同的时间来处理。否则,执行当然可能以不同的顺序发生,尽管提交是相同的。