【问题标题】:Java 7 Multithread Fork Join SchedulerJava 7 多线程 Fork Join 调度程序
【发布时间】:2014-02-08 17:45:22
【问题描述】:

我在 jre 1.7 中使用 Java fork/join 框架编写了一个多线程程序。该程序旨在在四叉树的所有节点中找到满足指定条件的某些点(四叉树中的每个叶节点可以填充无限数量的点,例如,可以是零或 1000)。我在 16 核处理器机器上测试了多线程程序与串行程序相比的加速比,而加速比仅为 1.3-1.5。下面是伪代码:

public class QuadtreeFindMultiThread extends RecursiveTask<IntArrayList> {
private Quadtree T;
private ObjectArrayList<Node> leaf_nodes;
private ObjectArrayList<Entry> candidatePoints;
private static int POINT_THRESHOLD = 50;
private static int NODE_THRESHOLD = 1;

public QuadtreeFindMultiThread(Quadtree T) {
    this.T = T
    this.leaf_nodes = T.get_nonempty_leaf_nodes();
    this.candidatePoints = new IntArrayList();
}

private QuadtreeFindMultiThread(Quadtree T, IntArrayList leaf_nodes) {
    this.T = T;
    this.leaf_nodes = leaf_nodes; // reference copy
    this.candidatePoints = new IntArrayList();
}

private IntArrayList QuadtreeFind() {
    //...
            //...
            return candidatePoints;
}

private int getPointNum(){
    int count = 0;
    for(Node node:this.leaf_nodes){
        count += node.getAllPoints().size();
    }
    return count;
}
@Override
public IntArrayList compute() {

    if (this.getPointNum() <= POINT_THRESHOLD || this.leaf_nodes.size() <= NODE_THRESHOLD) {// trivial problem, solve by single thread
        this.candidatePoints = QuadtreeFind();

    } else {// START: divide and conquer
    // Divide Step: partition this.leaf_nodes by direction: NW, NE, SW, SE
        Partition leaf_nodes to four quadrants: leaf_nodes_NW,
                    leaf_nodes_NE,
                    leaf_nodes_SW,
                    leaf_nodes_SE



    // Conquer Step
    QuadtreeFindMultiThread thread_NW = new QuadtreeFindMultiThread(
                this.T, leaf_nodes_NW);
    QuadtreeFindMultiThread thread_NE = new QuadtreeFindMultiThread(
                this.T, leaf_nodes_NE);
            QuadtreeFindMultiThread thread_SW = new QuadtreeFindMultiThread(
                this.T, leaf_nodes_SW);
            QuadtreeJoinMultiThread thread_SE = new QuadtreeFindMultiThread(
                this.T, leaf_nodes_SE);
        // fork three new sub threads
        thread_NE.fork();
        thread_SW.fork();
        thread_SE.fork();
        this.candidatePoints.addAll(thread_NW.compute()); // main thread
        this.candidatePoints.addAll(thread_NE.join());
        this.candidatePoints.addAll(thread_SW.join());
        this.candidatePoints.addAll(thread_SE.join());

    }// END: divide and conquer
    return this.candidatePoints;
}


}

我是 Java 多线程编程的新手,为什么这个程序在 16 核处理器机器上的加速效果如此糟糕?我还在我的笔记本电脑上测试了这个多线程程序,有 2 个内核和 2 个虚拟内核,加速也接近 1.3-1.5。我的笔记本电脑的多线程程序的性能有时甚至比 16 核处理器的机器更好。

看来fork/join framefork的默认调度策略是LIFO,怎么改成FIFO呢?

顺便说一句,我发现处理一些有很多点的叶子节点会占用很多处理时间。我可以修改 fork/join 调度程序,使其首先处理具有大量点的节点吗?因此它应该获得更好的性能。 谢谢!

【问题讨论】:

  • 更多节点会增加开销。您希望拥有尽可能少的节点以利用所有 CPU。
  • @PeterLawrey 所以增加 POINT_THRESHOLD 和 NODE_THRESHOLD 来增加线程粒度?但我认为性能不佳可能是由于粒度粗...如果我们增加粒度,那么该线程将处理更多的点,线程的处理时间与叶节点中的点数超线性。因此粗粒度可能会增加处理时间。
  • 如果你有 16 个 CPU,你至少需要 16-64 个线程。

标签: java multithreading


【解决方案1】:

没有太多关于这个框架的文档。目的当然很容易被误解。该框架用于平衡树(DAG)的递归分解。它不能很好地容忍误用,因为它最初是作为研究论文中的实验设计的。

框架想要左右拆分。 Left.fork()、right.compute()、left.join()。这样它就会沿着平衡树的叶子走。分叉的任务回到它的双端队列,希望被其他线程窃取。当一切按计划进行时,每个线程都会为其他线程分叉足够的任务并保持忙碌。

您正在做的是将三个任务放回双端队列,然后处理一个四边形。这并不能很好地分散工作。你最终可以得到的是几个线程有许多待处理的任务,而不是许多线程有很少的待处理任务。此框架无法正确进行负载平衡。

还有join()的问题。 Join() 需要上下文切换来释放线程以进行其他工作。该框架无法进行上下文切换,因此它为每个 join() 创建一个“继续线程”,然后为加入线程发出一个 wait()。对于许多连接,您可能有很多创建/销毁开销。 Java8 版本取消了“继续线程”,但最终经常停止(尤其是你这样做的方式。)

尝试重新设计以处理 DAG,看看会发生什么。使用 16 个线程,它应该可以正常工作。

【讨论】:

  • 嗨,edharned,感谢您的回答。我有几个问题: 1. 如何实现具有许多待处理任务的多个线程?你能给出一个具体的代码示例吗? :) 据我所知,fork/join 框架使用默认的 LIFO 调度策略,与 FIFO 调度策略相比,它大大减少了活动线程的数量; 2.为什么在每个递归步骤中划分为三个子任务会导致不平衡树,而二元划分会导致平衡树?就我而言,平衡与否应该与这里的 DOC_THRESHOLD 和 NODE_THRESHOLD 相关......
  • 很多问题。你需要生活在框架的边界内。它最初是作为研究论文的实验,而不是商业应用程序开发包。线程必须唤醒并寻找工作,因此无法平衡负载。在树中,你一半,fork/compute,这让其他线程有机会拿起分叉的任务并做同样的事情。每个线程都在做一个拆分到阈值。这使其他线程有机会参与。有关更多详细信息,请查看源代码。
【解决方案2】:

任何并行问题的诀窍是平衡两个不同的问题:

一方面,我们希望通过使任务尽可能小来获得最佳的负载平衡,这样我们就不必等待单个 CPU 完成其巨大的最后一个任务,而其他人都在等待。另一方面,调度细粒度任务会增加开销,因此我们希望使任务尽可能大,以尽可能少地增加调度开销。

诀窍是在这两个极端之间找到一个很好的平衡,这就是为什么 fork/join 程序通常有一个阈值,从这个阈值开始单线程执行任务。因此,正如 Peter 在他的评论中所观察到的,您需要调整两个阈值以获得最佳性能。

最佳阈值取决于很多因素——主要是问题的大小,但不同的计算机架构、内存等也会对其产生很大影响。解决这个问题的最佳方法是将阈值作为输入参数并以不同程度运行基准测试。

【讨论】:

  • 感谢您的回答!
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2010-11-17
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-06-12
  • 2019-05-05
  • 1970-01-01
相关资源
最近更新 更多