7.2 分支 合并框架


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

7.2.1 使用RecursiveTask


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

任务的拆分过程
![[Pasted image 20241203183443.png]\

下面是完整的代码

package lambdasinaction.chap7;  
  
import java.util.concurrent.ForkJoinPool;  
import java.util.concurrent.RecursiveTask;  
import java.util.stream.LongStream;  
  
class ForkJoinSumCalculator extends RecursiveTask<Long> {  
    private final long[] numbers;  
    private final int start;  
    private final int end;  
  
    public static final long THRESHOLD = 10000L;  
  
    public ForkJoinSumCalculator(long[] numbers) {  
        this(numbers, 0, numbers.length);  
    }  
  
    private ForkJoinSumCalculator(long[] numbers, int start, int end) {  
        this.numbers = numbers;  
        this.start = start;  
        this.end = end;  
    }  
  
    @Override  
    protected Long compute() {  
        int length = end - start;  
        if (length <= THRESHOLD) {  // 顺序计算  
            return computeSequentially();  
        }  
        // 创建一个子任务为前一半求和  
        ForkJoinSumCalculator leftTask = new ForkJoinSumCalculator(numbers, start, start + length / 2);  
        leftTask.fork();  
  
        // 处理后面部分  
        ForkJoinSumCalculator rightTask = new ForkJoinSumCalculator(numbers, start + length / 2, end);  
  
        // 将两部分的值合起来  
        Long rightResult = rightTask.compute();  
        Long leftResult = leftTask.join();  
        return rightResult + leftResult;  
    }  
  
    private long computeSequentially() {    // 顺序计算  
        long sum = 0L;  
        for (int i = start; i < end; i++) {  
            sum += numbers[i];  
        }  
        return sum;  
    }  
}  
  
public class Test02 {  
    public static long forkJoinSum(long n) {  
        long[] numbers = LongStream.rangeClosed(1, n).toArray();  
        ForkJoinSumCalculator calculator = new ForkJoinSumCalculator(numbers);  
        return new ForkJoinPool().invoke(calculator);  
    }  
  
    public static void main(String[] args) {  
        Test01.test(Test02::forkJoinSum, 10000000L);  
    }  
}

7.2.2 使用分支/合并框架的最佳做法


  • 对一个任务调用join方法会阻塞调用方,直到该任务做出结果。因此,有必要在两个子任务的计算都开始之后再调用它。否则,你得到的版本会比原始的顺序算法更慢更复杂,因为每个子任务都必须等待另一个子任务完成才能启动。
  • 不应该在RecursiveTask内部使用ForkJoinPool的invoke方法。相反,你应该始终直接调用compute或fork方法,只有顺序代码才应该用invoke来启动并行计算。
  • 对子任务调用fork方法可以把它排进ForkJoinPool。同时对左边和右边的子任务调用它似乎很自然,但这样做的效率要比直接对其中一个调用compute低。这样做你可以为其中一个子任务重用同一线程,从而避免在线程池中多分配一个任务造成的开销。
  • 调试使用分支/合并框架的并行计算可能有点棘手。特别是你平常都在你喜欢的IDE里面看栈跟踪(stack trace)来找问题,但放在分支-合并计算上就不行了,因为调用compute的线程并不是概念上的调用方,后者是调用fork的那个。
  • 和并行流一样,你不应理所当然地认为在多核处理器上使用分支/合并框架就比顺序计算快。我们已经说过,一个任务可以分解成多个独立的子任务,才能让性能在并行化时有所提升。所有这些子任务的运行时间都应该比分出新任务所花的时间长;一个惯用方法是把输入/输出放在一个子任务里,计算放在另一个里,这样计算就可以和输入/输出同时进行。此外,在比较同一算法的顺序和并行版本的性能时还有别的因素要考虑。就像任何其他Java代码一样,分支/合并框架需要“预热”或者说要执行几遍才会被JIT编译器优化。这就是为什么在测量性能之前跑几遍程序很重要,我们的测试框架就是这么做的。同时还要知道,编译器内置的优化可能会为顺序版本带来一些优势(例如执行死码分析——删去从未被使用的计算)。

7.2.3 工作窃取


![[Pasted image 20241203201735.png]]

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值