【问题标题】:Java fork/join framework logicJava fork/join 框架逻辑
【发布时间】:2012-08-31 16:01:51
【问题描述】:

这是对今天另一个问题的an answer 的“副作用”。与其说是实际问题,不如说是出于好奇。

Java SE 7 提供了 Oracle 所谓的“fork/join 框架”。这可能是一种将工作安排到多个处理器的优越方式。虽然我了解它应该如何工作,但我无法理解它的优越之处以及关于窃取工作的说法。

也许其他人更了解为什么这种方法是可取的(除了因为它有一个花哨的名字)。

fork/join 的底层原语是ForkJoinTasks,即Futures,其想法是或者立即执行工作[原文如此](措辞误导为“立即" 意味着它在主线程中同步发生,实际上这发生在低于某个阈值的Future) 内递归地将工作分成两个任务,直到达到阈值。

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)。

问题:

  1. 不是简单地将 sThreshold 个连续的项目塞进一个任务中,然后一个接一个地提交到线程池的任务队列,而是生成树状递归层次结构。这涉及为实际上什么都不做的 N 个子任务构建、引用和销毁 N+log2(N) 个对象。为什么这更胜一筹?

  2. 没有保留参考位置。处理器缓存和虚拟内存都不会被这样对待。为什么这更胜一筹?

  3. 除了在单处理器系统上,保证不会按照接近其原始顺序的顺序安排任务。如果它真的无关紧要,这可能没有问题,但它会使事情变得像例如栅栏或屏障几乎是不可行的。拥有类似栅栏的唯一方法是等待根对象完成,然后才提交新任务。这相当于一个完整的管道停顿(这正是您不希望发生的事情)。

  4. Oracle 文档声称这种方法实现了工作窃取因此比线程池更好。我没有看到这种情况发生。我所看到的只是将任务提交到普通线程池的一种非常复杂的方式。这应该如何神奇地实现工作窃取?


[1] 让我们不要把它弄得太复杂,并假设工作线程不会相互超越,任务都需要相同的时间来处理。否则,执行当然可能以不同的顺序发生,尽管提交是相同的。

【问题讨论】:

    标签: java multithreading concurrency java.util.concurrent fork-join


    【解决方案1】:

    当你使用ExecutorService时,将决定线程池中有多少线程,调度的任务之间没有区别以及这些任务创建的子任务
    ForkJoinPool 类根据 1) 可用处理器和 2) 任务需求来管理线程。
    在这种情况下,活动任务创建的子任务的调度方法与外部任务不同。
    我们通常有一个用于整个应用程序的 fork-join 池(与使用 ExecutorService 不同,在任何非平凡的应用程序中通常有超过 1 个),并且不需要 shutdown
    我还没有查看内部结构来给你一个更底层的解释,但是如果你看到here 有一个演示文稿和一个基准测试显示了显示承诺的并行度的测量值。

    更新:
    该框架解决了特定类型的问题(ExecutorService 更适合具有 CPU 和 I/O 活动混合的任务)。
    这里的基本思想是使用递归/分而治之的方法来保持 CPU 不断忙碌。这个想法是创建新任务(分叉)并暂停当前任务,直到新任务完成(加入),但创建新线程并且共享工作队列。
    因此,Fork-join 框架是通过创建有限数量的工作线程(与内核一样多)使用工作窃取来实现的。每个工作线程维护一个私有的双端工作队列。
    分叉时,工人将新任务推到其双端队列的头部。当等待或空闲时,worker 从其双端队列的头部弹出一个任务并执行它而不是休眠。
    如果 worker 的双端队列为空,则从另一个随机选择的 worker 的双端队列尾部窃取一个元素。
    我建议阅读Data Parallelism in Java 并自己做一些基准测试以说服自己。理论只有在一定程度上是好的。之后进行测量,看看是否有显着的性能优势

    【讨论】:

    • 所以基本上这一切都归结为“根据核心数量自动选择数量或工人”,剩下的只是编写提交代码和营销等等的花哨方式?或者说得更积极一点,它是为了避免“过于幼稚”的用户代码过度线程化(即几个 ExecutorServices 的线程数比内核数多得多)。
    • 您对工作窃取(从另一个线程的队列头弹出)的描述是完全正确的。我的问题是我看不出这与以任何方式递归细分有何关系。这对于最简单的任务提交方式(例如,简单的循环分配,甚至是一个不会做太多但减少锁争用的共享队列)或一般的 Futures 来说同样适用。
    • 你喜欢的演示文稿表明它就像我想的那样:“笨拙的代码 => 人们不会打扰它”——换句话说,分而治之方法在性能或工作窃取方面没有任何增加(这在未来的实现中独立发生)。相反,这种方法是一种花哨的编程模型,因此人们实际上想要使用它。感谢您的参考和解释。
    • 相当老的帖子,但偶然发现了这个讨论:我不认为“花哨的编程模型”涵盖了所有内容。 (1)F/J框架提供任务,用户级轻量级,比OS在CPU上调度线程更轻量级(开销更小); (2) 工作窃取是一种基于拉动的负载平衡机制(空闲工作人员窃取任务),因此优于未充分利用的工作人员可能缺乏的工作共享; (3) fork-join 是并发管理的一个常见概念,我们也可以在 POSIX 线程中看到它。它类似于英特尔的 TBB,顺便说一句,这是一个很好的阅读。
    【解决方案2】:

    让我从一篇批评框架的文章 [是的,我写的] 开始。 A Java Fork-Join Calamity

    现在回答你的问题:

    1. 不是。框架想要处理 DAG。这就是设计结构。

    2. 不是。正如文章所述,Java 应用程序对缓存、内存等一无所知,因此这些假设是错误的。

    3. 是的。这正是发生的事情。停顿是如此普遍,以至于框架需要创建“延续线程”来保持移动。这篇文章在此处引用了一个需要 700 多个延续线程的问题。

    4. 我当然同意代码很复杂。 Scatter-gather 比应用程序的工作窃取要好得多。至于文档,什么文档?甲骨文没有提供详细信息。这完全是为了使用该框架。

    还有其他选择。

    【讨论】:

    • This articleAPI docs 提出上述关于窃取工作的声明。我只是不明白这是怎么发生的。
    • 不确定“这个”是什么意思?工作窃取在操作系统中运行良好。工作共享在应用程序中运行良好。 F/J 框架的问题在于它是一个试图模仿操作系统或伪操作系统但没有任务控制、错误恢复、文档等的应用程序。
    • 我不怀疑工作窃取工作正常,在应用程序中实施工作窃取甚至没有任何根本性的错误(尽管它不是我更喜欢使用的)。然而,文档指出,FW “与众不同,因为它使用了工作窃取算法”,这不是递归细分可以提供的东西(尽管它们听起来像那样)。这是线程池实现的一个实现细节(至少,我看不出它有什么不同)。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2016-01-05
    • 1970-01-01
    • 2013-03-03
    • 1970-01-01
    • 2015-07-23
    • 1970-01-01
    • 2011-05-15
    相关资源
    最近更新 更多