【问题标题】:Split a spliterator into N spliterators将一个拆分器拆分为 N 个拆分器
【发布时间】:2015-02-25 09:59:57
【问题描述】:

在分析一位同事几年前的 Java 7 代码时,我发现他实现了一个用于遍历数据的实用程序,可能是并行的。他将其命名为Range,并扩展了Iterator 接口。它的一些新方法令人尴尬地熟悉:

  • int size() 将给出范围的确切大小;
  • Range split() 会将范围分成 2 部分,最好但不一定在大小上相似(修改当前范围并创建一个新范围);
  • Range[] split(int n) 会将范围分成 N 个子范围,可能会尝试使它们尽可能均匀。
  • 虽然void remove() 是从Iterator 那里出现的,但它的子类型只是抛出UnsupportedOperationException

我对自己说的是,这绝对是我们现在可以使用(缩小的)拆分器来做的事情,从而利用流 API。但是,Java 8 中的拆分器并没有提供一种简单的方法来拆分为 n 部分。

使用递归函数,我们可以将其拆分为 n = 2^L 部分和 L 递归级别,但是当 n 不是 2 的幂时,这种方法并不简单,更不用说感觉不自然为了效果而保留效用函数。

也有人可能会说简单地避免使用拆分器并让流在实际处理过程中执行由 fork 引起的拆分,但 ForkJoin 策略可能不适用于该任务,并且不保证它会使用我们特别希望用于该工作的线程数。事实上,可能会出现在少数元素上执行繁重任务的情况。

问题总结如下:拥有一个至少具有 SIZED 和 SUBSIZED 特征的拆分器,如何将其拆分为确切数量的拆分器?

【问题讨论】:

    标签: java java-8


    【解决方案1】:

    实现它的方法是编写一个拆分器wrapper,它使用自己的拆分策略,它甚至不那么老套并且与现有的 F/J 支持框架互操作。你失去的是利用原生随机访问结构的能力;您只能通过迭代内部拆分器的数据集来拆分。

    你可以参考我的earlier answer,我在这里展示了分成预定大小的批次的代码;只需适当的构造函数调用,就可以很容易地使其适应您的情况。

    【讨论】:

    • 奇怪的是,我在阅读您的博文后不久就提出了这个问题。尽管它使我更接近解决方案,但它仍然专注于固定批次的大小而不是处理核心的数量。尽管如此,我现在会尝试一下您的建议,并在我有结果后报告。
    • 这是我的观点,但我认为相同的方法很容易适应核心计数驱动的拆分策略。在我看来,你真正的问题将是这种方法的顺序性。一个 SIZED 拆分器最有可能由一个数组支持,并且利用该事实的实现会明显更好(在空间和时间上 O(1) 拆分而不是 O(n))。
    • 你是对的!事实上,我们当前的实现要么由ArrayList 支持,要么由Lucene IndexReader 支持。提前知道这些细节肯定会提高性能,但如果在这里也有一个通用的解决方案,那就太好了。
    • 我对 IndexReader 支持的并行化有个人经验(事实上这正是我编写这些类的原因)。由于 Lucene 操作是重量级的东西(100 µs 的查找并不罕见),因此即使在顺序模式下也可以进行并行化。请注意,在这种情况下,将核心数量与您的批次数量完全匹配并不那么重要。给每个批次足够的工作(比如 10 毫秒),协调/切换开销就消失了。
    【解决方案2】:

    “……但 ForkJoin 策略可能不足以完成任务”

    对我来说听起来像是过早的优化。您想手动实现复杂的事情,因为现有的策略可能不够......

    “……并且不保证它会使用我们特别希望用于该工作的线程数。”

    确实没有保证,但当前的流实现使用common Fork/Join pool 哪个can be configured to a desired number of threads。将指定数量的线程专用于任务正是您所要求的策略。

    “事实上,在少数元素上执行繁重任务的情况可能存在。”

    我认为 F/J 框架的工作方式没有任何矛盾。它将尝试拆分,直到达到所需的并行度。如果这意味着每个线程只处理单个项目,那么它会这样做。

    此时,值得注意的是,与内核数量相匹配的默认并行度足以满足任何不涉及阻塞的计算任务,无论处理单个项目需要多少时间。只要每个线程都有其工作负载,就不可能超出实际硬件执行单元的数量。

    换句话说,F/J 框架实现了您想要自己实现的策略(或优于您将实现的策略),这使我们回到了第一点,即过早优化。

    【讨论】:

    • 您已经陈述了一些优点。但是考虑另一个用例,如果我的N 小于池中的线程数怎么办? AFAIK,我无法为每个任务定义线程限制。我在它周围看到的一个技巧是收集流并将其传递给新的 ForkJoinPool。
    • 如果您的任务比线程少,您必须拆分任务本身以获得更多并行性,这是没有人可以为您做的事情,因为您是唯一知道实际任务以及如何执行的人如果可能的话,将它们分开。我看不出收集项目并传递到另一个池应该如何帮助你,这可能会在你的监视工具中创建一个看起来很时髦的线程实例,但对于并行性没有任何好处,因为第二个池的线程都必须等待第一个池执行的收集完成,这样它们就不会同时运行。
    • 考虑目标并行度为 3。通过应用硬连线的中间拆分策略,ForkJoin 将无法进行最佳的平衡拆分。这是否至少为尝试 OP 的方法提供了足够的理由?
    • While processing the first tiny chunks concurrently, enough information can be gathered to find out whether the follow-up bigger chunks should be subdivided as well or not.---也许在未来的版本中他们会利用这一点。我实际上在lambda-dev 上询问了关于AbstractSpliterator 中使用的低效政策的这种政策。但是,正如您所指出的,一个块的时间并不是其他块时间的可靠预测指标。
    • 好吧,也许你最好使用自定义实现,尤其是。但是,对于随机访问源并且当您的目标并行度确实是质数时,它总是与移动目标进行比较。我也不满意 API 强制 O(log(n)) 复杂性只是为了拆分,但我对自定义实现的有用性表示怀疑。开发人员可能会做错很多事……
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2021-02-11
    • 1970-01-01
    • 2011-03-12
    • 2013-09-29
    • 1970-01-01
    • 1970-01-01
    • 2012-01-01
    相关资源
    最近更新 更多