为了账号安全,请及时绑定邮箱和手机立即绑定

使用分支/合并框架执行并行求和

标签:
Java

分支/合并框架

分支/合并框架的目的是以递归方式将可以并行的任务拆分成更小的任务,然后将每个子任
务的结果合并起来生成整体结果。它是ExecutorService接口的一个实现,它把子任务分配给
线程池(称为ForkJoinPool)中的工作线程。首先来看看如何定义任务和子任务。

使用RecursiveTask

要把任务提交到这个池,必须创建RecursiveTask的一个子类,其中R是并行化任务(以 及所有子任务)产生的结果类型,或者如果任务不返回结果,则是RecursiveAction类型(当 然它可能会更新其他非局部机构)。要定义RecursiveTask,只需实现它唯一的抽象方法 compute:

protected abstract R compute();

这个方法同时定义了将任务拆分成子任务的逻辑,以及无法再拆分或不方便再拆分时,生成 单个子任务结果的逻辑。

正由于此,这个方法的实现类似于下面的伪代码:

if (任务足够小或不可分) { 顺序计算该任务
} else {
     将任务分成两个子任务
     递归调用本方法,拆分每个子任务,等待所有子任务完成
     合并每个子任务的结果
}

一般来说并没有确切的标准决定一个任务是否应该再拆分,但有几种试探方法可以帮助你做
出这一决定,递归的任务拆分过程如图下图所示:

5c335b310001a25a07380575.jpg

你可能已经注意到,这只不过是著名的分治算法的并行版本而已。这里举一个用分支/合并
框架的实际例子,让我们试着用这个框架为一个数字范围(这里用一个
long[]数组表示)求和。如前所述,你需要先为RecursiveTask类做一个实现,就是下面代码
ForkJoinSumCalculator。

用分支/合并框架执行并行求和

package java8.java8example;import java.util.concurrent.RecursiveTask;/**
 * @desctiption:
 * @author: yinghuaYang
 * @date: 2019/1/7
 */public class ForkJoinSumCalculator extends RecursiveTask<Long> {    /**
     * 要求和的数组
     */
    private final long[] numners;    /**
     * 子任务处理的数组的起始位置
     */
    private final int start;    /**
     * 子任务处理的数组的终止位置
     */
    private final int end;    /**
     * 不再将任务分解为子任务的数组大小
     */
    public static final long THRESHOLD = 10000;    /**
     * 公共构造方法用于创建主任务
     * @param numners
     */
    public ForkJoinSumCalculator(long[] numners) {        this(numners,0,numners.length);
    }    /**
     * 私有构造方法用于以递归方式为主任务创建子任务
     * @param numners
     * @param start
     * @param end
     */
    private ForkJoinSumCalculator(long[] numners, int start, int end) {        this.numners = numners;        this.start = start;        this.end = end;
    }    /**
     * 覆盖RecursiveTask抽象方法
     *
     * 该任务负责求和的步伐的大小
     * @return
     */
    @Override
    protected Long compute() {        int length = end - start;         /*如果大小小于或等于阈值,顺序执行计算结果*/
        if (length <= THRESHOLD) {            return computeSequentially();
        }

        ForkJoinSumCalculator leftTask = new ForkJoinSumCalculator(numners,start,start+length/2);
        leftTask.fork();        /*创建一个任务为数组的后一半求和*/
        ForkJoinSumCalculator rightTask = new ForkJoinSumCalculator(numners,start+length/2,end);        /*同步执行第二个子任务,有可能允许进一步递归划分*/
        Long rightResult = rightTask.compute();        /*读取第一个子任务的结果,如果尚未完成就等待*/
        Long leftResult = leftTask.join();        /*该任务的结果是两个子任务结果的组合*/
        return leftResult + rightResult;
    }    /**
     * 在子任务不再可分时及时结果的简单写法
     * @return
     */
    private long computeSequentially() {        long sum = 0;        for (int i=0;i<end;i++) {
            sum += numners[i];
        }        return sum;
    }

}


现在编写一个方法来并行对前n个自然数求和就很简单了。你只需把想要的数字数组传给
ForkJoinSumCalculator的构造函数:

package java8.java8example;import java.util.concurrent.ForkJoinPool;import java.util.concurrent.ForkJoinTask;import java.util.stream.LongStream;/**
 * @desctiption:
 * @author: yinghuaYang
 * @date: 2019/1/7
 */public class ForkJoinSumCalculatorMain {    public static long forkJoinSum(long n) {        long[] numbers = LongStream.rangeClosed(1,n).toArray();

        ForkJoinTask<Long> task = new ForkJoinSumCalculator(numbers);        return new ForkJoinPool().invoke(task);
    }    public static void main(String[] args) {        long totalResult = forkJoinSum(10000L);
        System.out.println(totalResult);
    }
}

注意:

上面用了一个LongStream来生成包含前n个自然数的数组,然后创建一个ForkJoinTask (RecursiveTask的父类),并把数组传递给代码所示ForkJoinSumCalculator的公共 构造函数。最后,你创建了一个新的ForkJoinPool,并把任务传给它的调用方法 。

在 ForkJoinPool中执行时,最后一个方法返回的值就是ForkJoinSumCalculator类定义的任务结果。 请注意在实际应用时,使用多个ForkJoinPool是没有什么意义的。正是出于这个原因,一般来说把它实例化一次,然后把实例保存在静态字段中,使之成为单例,这样就可以在软件中任 何部分方便地重用了。这里创建时用了其默认的无参数构造函数,这意味着想让线程池使用JVM 能够使用的所有处理器。更确切地说,该构造函数将使用Runtime.availableProcessors的 返回值来决定线程池使用的线程数。请注意availableProcessors方法虽然看起来是处理器, 但它实际上返回的是可用内核的数量,包括超线程生成的虚拟内核。

运行ForkJoinSumCalculator,当把ForkJoinSumCalculator任务传给ForkJoinPool时,这个任务就由池中的一个线程 执行,这个线程会调用任务的compute方法。该方法会检查任务是否小到足以顺序执行,如果不 够小则会把要求和的数组分成两半,分给两个新的ForkJoinSumCalculator,而它们也由 ForkJoinPool安排执行。

因此,这一过程可以递归重复,把原任务分为更小的任务,直到满足 不方便或不可能再进一步拆分的条件(本例中是求和的项目数小于等于10 000)。这时会顺序计 算每个任务的结果,然后由分支过程创建的(隐含的)任务二叉树遍历回到它的根。接下来会合 并每个子任务的部分结果,从而得到总任务的结果。

这一过程如图下图所示:

5c335b31000176c807840383.jpg


作者:杨英华

出处:https://www.cnblogs.com/yangyinghua/p/10234625.html  

本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。


点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消