并行编程之Fork/Join框架
2013-11-27 16:55
176 查看
一、概述:
在实际情况中,很多时候我们都需要面对经典的“分治”问题。要解决这类问题,主要任务通常被分解为多个任务块(分解阶段),其后每一小块任务被独立并行计算。一旦计算任务完成,每一快的结果会被合并或者解决(解决阶段)。“分治”问题可以很容易地通过Callable线程的Executor接口来解决。通过为每个任务实例化一 个Callable实例,并在ExecutorService类中汇总计算结果来得出最终结果可以实现这一目的。
那么自然而然想到的问题就是,如果这一 接口已经做得不错了,我们为什么还需要Java 7的其他框架?使用ExecutorService和Callable的主要问题是,Callable实例在本质上是阻塞的。一旦一个Callable实例开始执行,其他所有Callable都会被阻塞。由于队列后面的Callable实例在前一实例未执行完成的时候不会被执行,因此许多资源无法得到利用。Fork/Join框架被引入来解决这一并行问题,可以处理更细粒度的并行计算,而Executor解决的是并发问题。(并发和并行的区别就是一个处理器同时处理多个任务和多个处理器或者是多核的处理器同时处理多个不同的任务)。ForkJoin是适用于多核环境的轻量级并行框架。目标是在多核系统下,通过并行运算,充分利用多处理器,提高效率与加速运行。
二、ForkJoin框架基本结构:
ForkJoinPool本身实现了ExecutorService接口,负责调度执行ForkJoinTask。ForkJoinTask是提交给ForkJoinPool 执行的任务,本身也实现了Future接口。ForkJoinTask有两个子类RecursiveAction和RecursiveTask。 RecursiveAction没有返回值(只需fork);RecursiveTask有返回值(需要合并)。类似于Runnable、Callable一样。没有返回值一般意味着所有子任务都执行完了即可,中间的子任务不需要join了。其实要不要返回值都可以实现,有返回值可以直接合并,没有返回值可以把结果保存在共享的数据上。而我们要做的是实现自己要完成的任务,只需要继承其一,并覆盖抽象方法compute()。在这个方法中实现自己的任务,递归分解任务。
ForkJoin框架的核心是ForkJoinPool类,实现了work-stealing算法,用于执行ForkJoinTask类型的任务(也就是按照该算法调度线程与任务,当然还负责解决好相关的一些其它问题)。
除了几个其他API方法以外,ForkJoinTask有两个主要的方法:
* fork () – 这个方法决定了ForkJoinTask的异步执行,凭借这个方法可以创建新的任务。
* join () – 该方法负责在计算完成侯返回结果,因此允许一个任务等待另一任务执行完成。分支/合并的完整过程如下:
三、ForkJoin编程范式:
将问题递归地分解为较小的子问题,并行处理这些子问题,然后合并结果,如:
work-stealing是一种任务调度方法,由多个工作线程组成,每个工作线程用一个双端队列维护一组任务。Fork的时候是把任务加到队列的头部,而不像一般的线程池那样是加到任务队列末尾。工作线程选择头部最新的任务来执行。当工作线程没有任务可执行时,它会尝试从其它线程的任务队列尾部窃取一个任务执行。如果没有任务执行了并且窃取其它任务失败,那么工作线程停止。
这种方法的优点是减少了争用,因为工作线程从头获取任务,而窃取线程从尾部窃取任务。另一个优点是递归的分治法使得早期产生的是较大的任务单元,而窃取到较大任务会进一步递归分解,因此也减少了尾部窃取的次数。另外,父任务很可能要等待子任务(join),所以从队列头部子任务开始执行也是一种优化。
总之,它会使用有限的线程执行大量任务,同时保持各线程的任务都处于繁忙的执行状态,而尽量不让线程处于等待状态。为了做到这点可能会从其它线程的任务队列中窃取任务来执行,所以叫work-stealing。
五、使用ForkJoin的步骤:
ForkJoin框架替我们完成了一些工作,那么我们使用时还要完成哪些工作:
1)如何执行单个任务。如果只切分出一个任务执行,就相当于单线程顺序执行。
2)如何递归地切分任务(以及任务切分后是否需要合并结果)
3)切分粒度多少合适(最小任务单元)
这些具体表现在:继承ForkJoinTask的一个子类,并实现抽象方法compute()。在这个方法中实现自己的任务,递归分解任务。这些准备好之后就可以启动了:创建一个表示全部任务的ForkJoinTask对象,创建一个ForkJoinPool的实例,把task作为参数执行ForkJoinPool的invoke方法。
在ForkJoin任务外部执行总任务:execute异步执行任务,没有返回结果void;invoke执行任务并等待返回结果,结果是特定类型;submit执行一个任务,返回ForkJoinTask(实际上是作为Future对象返回)。一般应该在外部使用invoke调用执行总任务。而execute和submit只是为了实现ExecutorService规定的相关语义,invoke是ForkJoin中特有的。在ForkJoinTask内部递归执行的过程中:fork是异步执行,invoke是等待任务执行完成。
六、程序实例(将较大数目的浮点数组进行排序)
执行结果
七、对分支合并框架的几个问题归纳
1、影响ForkJoin加速效果的因素
1)并发数,即线程数。一般是可用的cpu数,默认就是这个(比如Runtime.getRuntime().availableProcessors()返回处理器的数量),一般表现很好。
2)任务切分的粒度。如果切分粒度等于总任务量,一个任务执行,就相当于单线程顺序执行。每个任务执行的计算量,太大的话加速效果有限,不能发挥到最好。相反,太小的话,消耗在任务管理的成本占了主要部分,导致还不如顺序执行的快。需要适当平衡二者,而总的执行时间还与任务的规模有关。
2、分治、并行、可伸缩的思考:
这三者关系很亲密。分治思想(divide-and-conquer)是一种简单朴素的思想,很多问题都可以这样解决。ForkJoin就相当于分治法的并行版本。 分治本身只是解决问题的思想,既可以顺序执行也可以并行执行,但是在并行环境中更加有效,因为可以并行处理子问题。而在并行方面,可并行处理问题要么是彼此完全独立的问题,要么是可分解单独处理的问题。可伸缩性又和能否并行处理紧密相关,因为如果不能并行处理就要受到单机处理能力的限制,也就难以伸缩了。
3、ForkJoin与MapReduce两个并行计算框架的区别?
MapReduce是把大数据集切分成小数据集,并行分布计算后再合并;ForkJoin是将一个问题递归分解成子问题,再将子问题并行运算后合并结果。
二者共同点:都是用于执行并行任务的。基本思想都是把问题分解为一个个子问题分别计算,再合并结果。应该说并行计算都是这种思想,彼此独立的或可分解的。从名字上看Fork和Map都有切分的意思,Join和Reduce都有合并的意思,比较类似。
区别:1)环境差异,分布式 vs 单机多核:ForkJoin设计初衷针对单机多核(处理器数量很多的情况)。MapReduce一开始就明确是针对很多机器组成的集群环境的。也就是说一个是想充分利用多处理器,而另一个是想充分利用很多机器做分布式计算。这是两种不同的的应用场景,有很多差异,因此在细的编程模式方面有很多不同。2)编程差异:MapReduce一般是:做较大粒度的切分,一开始就先切分好任务然后再执行,并且彼此间在最后合并之前不需要通信。这样可伸缩性更好,适合解决巨大的问题,但限制也更多。ForkJoin可以是较小粒度的切分,任务自己知道该如何切分自己,递归地切分到一组合适大小的子任务来执行,因为是一个JVM内,所以彼此间通信是很容易的,更像是传统编程方式。
4、ForkJoinPool与一般的ExecutorService实现的差别?
ForkJoin实现了ExecutorService接口,这个接口就是用来把任务交给线程池中的工作线程去执行。ForkJoin也是一个ExecutorService,但区别在于ForkJoin使用了work-stealing算法,普通的线程池是按FIFO的方式执行,而ForkJoin优先执行(由其它任务)后创建的子任务。对于大部分会产生子任务的任务模式,ForkJoin的处理实现会很高效。如果设置了异步模式, ForkJoin也可能适合执行事件类型(不需要join)的任务。另外,ForkJoinPool需要在程序结束时显式的停止,因为其中所有的线程都处于守护模式。
在实际情况中,很多时候我们都需要面对经典的“分治”问题。要解决这类问题,主要任务通常被分解为多个任务块(分解阶段),其后每一小块任务被独立并行计算。一旦计算任务完成,每一快的结果会被合并或者解决(解决阶段)。“分治”问题可以很容易地通过Callable线程的Executor接口来解决。通过为每个任务实例化一 个Callable实例,并在ExecutorService类中汇总计算结果来得出最终结果可以实现这一目的。
那么自然而然想到的问题就是,如果这一 接口已经做得不错了,我们为什么还需要Java 7的其他框架?使用ExecutorService和Callable的主要问题是,Callable实例在本质上是阻塞的。一旦一个Callable实例开始执行,其他所有Callable都会被阻塞。由于队列后面的Callable实例在前一实例未执行完成的时候不会被执行,因此许多资源无法得到利用。Fork/Join框架被引入来解决这一并行问题,可以处理更细粒度的并行计算,而Executor解决的是并发问题。(并发和并行的区别就是一个处理器同时处理多个任务和多个处理器或者是多核的处理器同时处理多个不同的任务)。ForkJoin是适用于多核环境的轻量级并行框架。目标是在多核系统下,通过并行运算,充分利用多处理器,提高效率与加速运行。
二、ForkJoin框架基本结构:
ForkJoinPool本身实现了ExecutorService接口,负责调度执行ForkJoinTask。ForkJoinTask是提交给ForkJoinPool 执行的任务,本身也实现了Future接口。ForkJoinTask有两个子类RecursiveAction和RecursiveTask。 RecursiveAction没有返回值(只需fork);RecursiveTask有返回值(需要合并)。类似于Runnable、Callable一样。没有返回值一般意味着所有子任务都执行完了即可,中间的子任务不需要join了。其实要不要返回值都可以实现,有返回值可以直接合并,没有返回值可以把结果保存在共享的数据上。而我们要做的是实现自己要完成的任务,只需要继承其一,并覆盖抽象方法compute()。在这个方法中实现自己的任务,递归分解任务。
ForkJoin框架的核心是ForkJoinPool类,实现了work-stealing算法,用于执行ForkJoinTask类型的任务(也就是按照该算法调度线程与任务,当然还负责解决好相关的一些其它问题)。
除了几个其他API方法以外,ForkJoinTask有两个主要的方法:
* fork () – 这个方法决定了ForkJoinTask的异步执行,凭借这个方法可以创建新的任务。
* join () – 该方法负责在计算完成侯返回结果,因此允许一个任务等待另一任务执行完成。分支/合并的完整过程如下:
三、ForkJoin编程范式:
将问题递归地分解为较小的子问题,并行处理这些子问题,然后合并结果,如:
if (my portion of the work is small enough) do the work directly else split my work into two pieces invoke the two pieces and wait for the results四、work-stealing算法
work-stealing是一种任务调度方法,由多个工作线程组成,每个工作线程用一个双端队列维护一组任务。Fork的时候是把任务加到队列的头部,而不像一般的线程池那样是加到任务队列末尾。工作线程选择头部最新的任务来执行。当工作线程没有任务可执行时,它会尝试从其它线程的任务队列尾部窃取一个任务执行。如果没有任务执行了并且窃取其它任务失败,那么工作线程停止。
这种方法的优点是减少了争用,因为工作线程从头获取任务,而窃取线程从尾部窃取任务。另一个优点是递归的分治法使得早期产生的是较大的任务单元,而窃取到较大任务会进一步递归分解,因此也减少了尾部窃取的次数。另外,父任务很可能要等待子任务(join),所以从队列头部子任务开始执行也是一种优化。
总之,它会使用有限的线程执行大量任务,同时保持各线程的任务都处于繁忙的执行状态,而尽量不让线程处于等待状态。为了做到这点可能会从其它线程的任务队列中窃取任务来执行,所以叫work-stealing。
五、使用ForkJoin的步骤:
ForkJoin框架替我们完成了一些工作,那么我们使用时还要完成哪些工作:
1)如何执行单个任务。如果只切分出一个任务执行,就相当于单线程顺序执行。
2)如何递归地切分任务(以及任务切分后是否需要合并结果)
3)切分粒度多少合适(最小任务单元)
这些具体表现在:继承ForkJoinTask的一个子类,并实现抽象方法compute()。在这个方法中实现自己的任务,递归分解任务。这些准备好之后就可以启动了:创建一个表示全部任务的ForkJoinTask对象,创建一个ForkJoinPool的实例,把task作为参数执行ForkJoinPool的invoke方法。
在ForkJoin任务外部执行总任务:execute异步执行任务,没有返回结果void;invoke执行任务并等待返回结果,结果是特定类型;submit执行一个任务,返回ForkJoinTask(实际上是作为Future对象返回)。一般应该在外部使用invoke调用执行总任务。而execute和submit只是为了实现ExecutorService规定的相关语义,invoke是ForkJoin中特有的。在ForkJoinTask内部递归执行的过程中:fork是异步执行,invoke是等待任务执行完成。
六、程序实例(将较大数目的浮点数组进行排序)
import java.util.concurrent.ForkJoinPool; import java.util.concurrent.RecursiveAction; public class ParallelMergeSort { private static ForkJoinPool threadPool; private static final int THRESHOLD = 16; private static void sort(Comparable[] objectArray) { Comparable[] destArray = new Comparable[objectArray.length]; threadPool.invoke(new SortTask(objectArray, destArray, 0, objectArray.length - 1)); } static class SortTask extends RecursiveAction { private static final long serialVersionUID = 1L; private Comparable[] sourceArray; private Comparable[] destArray; private int lowerIndex; private int upperIndex; public SortTask(Comparable[] sourceArray, Comparable[] destArray, int lowerIndex, int upperIndex) { this.sourceArray = sourceArray; this.destArray = destArray; this.lowerIndex = lowerIndex; this.upperIndex = upperIndex; } @Override protected void compute() { if (upperIndex - lowerIndex < THRESHOLD) { insertionSort(sourceArray, lowerIndex, upperIndex); return; } int midIndex = (lowerIndex + upperIndex) >>> 1; invokeAll( new SortTask(sourceArray, destArray, lowerIndex, midIndex), new SortTask(sourceArray, destArray, midIndex + 1, upperIndex)); merge(sourceArray, destArray, lowerIndex, midIndex, upperIndex); } private void merge(Comparable[] sourceArray, Comparable[] destArray, int lowerIndex, int midIndex, int upperIndex) { if (sourceArray[midIndex].compareTo(sourceArray[midIndex + 1]) <= 0) { return; } System.arraycopy(sourceArray, lowerIndex, destArray, lowerIndex, midIndex - lowerIndex + 1); int i = lowerIndex; int j = midIndex + 1; int k = lowerIndex; while (k < j && j <= upperIndex) { if (destArray[i].compareTo(sourceArray[j]) <= 0) { sourceArray[k++] = destArray[i++]; } else { sourceArray[k++] = sourceArray[j++]; } } System.arraycopy(destArray, i, sourceArray, k, j - k); } private void insertionSort(Comparable[] objectArray, int lowerIndex, int upperIndex) { for (int i = lowerIndex + 1; i <= upperIndex; i++) { int j = i; Comparable tempObject = objectArray[j]; while (j > lowerIndex && tempObject.compareTo(objectArray[j - 1]) < 0) { objectArray[j] = objectArray[j - 1]; --j; } objectArray[j] = tempObject; } } } public static Double[] createRandomData(int length) { Double[] data = new Double[length]; for (int i = 0; i < data.length; i++) { data[i] = length * Math.random(); } return data; } public static void main(String[] args) { int processors = Runtime.getRuntime().availableProcessors(); System.out.println("No of processors: " + processors); threadPool = new ForkJoinPool(processors); Double[] data = createRandomData(1000); System.out.println("Orginal unsorted data:"); int k = 0; for (Double d : data) { if (++k % 100 == 0) { System.out.println(); } System.out.printf("%3.2f ", (double) d); } sort(data); System.out.println("\n\nSorted Array:"); for (Double d : data) { if (++k % 100 == 0) { System.out.println(); } System.out.printf("%3.2f ", (double) d); } } }
执行结果
No of processors: 4 Orginal unsorted data: 498.57 517.40 971.53 831.27 812.89 235.02 692.14 998.85 693.03 79.25 805.36 222.89 658.93 284.86 98.20 77.10 523.99 159.91 257.43 201.68 820.57 862.75 990.14 443.81 732.19 685.80 834.08 359.13 839.29 450.15 84.23 488.96 513.66 588.37 993.48 130.19 981.20 373.06 293.03 22.35 913.56 534.71 121.88 491.02 906.21 204.65 655.24 106.85 353.75 717.98 78.02 846.63 43.14 315.58 62.08 75.51 250.87 196.06 575.27 408.94 252.66 685.47 675.01 658.34 932.17 516.52 921.01 616.23 610.17 338.59 179.17 394.61 306.02 616.43 882.75 394.67 390.74 17.25 267.10 756.31 994.13 235.12 700.59 312.76 587.88 956.02 997.26 658.70 882.57 328.23 515.45 929.35 548.51 870.16 921.96 283.63 298.57 221.27 732.78 。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。 331.82 80.82 52.01 791.17 576.10 8.15 329.17 412.28 564.97 32.58 837.01 105.83 454.05 330.94 433.35 184.36 594.98 988.70 377.10 962.70 529.03 494.03 966.69 533.98 993.06 939.03 612.46 102.32 446.33 606.82 514.29 373.80 928.90 808.10 995.73 394.98 769.77 671.36 331.86 272.38 777.70 88.08 600.64 406.18 67.87 167.51 685.99 228.14 604.34 365.31 65.94 946.36 871.38 916.13 430.03 468.39 825.76 322.06 487.87 499.32 428.04 427.58 553.97 793.28 602.01 45.85 624.44 673.90 430.22 192.29 696.48 233.88 756.98 728.17 24.88 73.42 141.41 620.62 857.50 44.14 279.49 84.68 947.38 227.52 597.12 589.02 799.56 296.14 705.58 79.04 178.61 480.75 806.17 799.37 876.22 598.22 932.67 782.16 118.12 982.55 128.04 Sorted Array: 0.57 0.81 1.07 6.44 6.45 8.15 10.27 10.84 11.01 14.64 14.98 17.12 17.25 17.92 18.43 18.65 21.12 21.39 22.22 22.35 22.92 24.88 25.35 26.87 28.02 32.58 34.71 35.45 35.69 37.78 39.34 40.24 40.91 42.58 42.77 42.97 43.14 43.93 44.14 44.49 44.91 45.85 47.44 47.63 52.01 52.43 53.51 54.80 55.33 59.02 60.55 61.38 62.08 63.72 64.20 64.81 65.94 66.95 67.87 69.90 71.55 71.68 71.87 73.42 74.41 75.51 77.10 78.02 79.04 79.25 80.71 80.82 81.11 81.35 81.43 84.23 84.68 86.91 87.42 88.08 91.04 94.95 96.15 96.45 97.00 98.20 99.60 100.50 100.58 101.08 102.09 102.32 105.54 105.83 106.85 107.54 107.61 114.78 116.52 。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。 917.31 917.51 917.64 917.75 918.93 918.96 920.18 920.78 921.01 921.67 921.96 924.36 924.70 924.74 928.41 928.90 928.95 929.35 931.55 932.17 932.67 935.71 936.45 936.65 937.74 939.03 939.21 939.37 940.55 940.66 941.01 941.39 942.73 944.34 944.88 945.30 946.36 947.38 948.32 949.26 949.44 950.40 950.86 951.13 953.02 953.86 954.12 955.79 956.02 956.89 957.69 959.43 959.75 960.26 961.79 962.70 963.08 964.18 964.49 965.01 966.21 966.69 967.62 969.98 971.53 971.94 972.79 973.62 974.46 974.68 975.85 976.57 978.54 979.41 979.52 981.20 981.64 982.36 982.55 984.24 984.66 985.77 986.44 987.94 988.70 989.47 990.14 992.77 992.87 993.06 993.48 993.92 994.13 995.39 995.73 995.73 996.55 997.26 998.33 998.85 999.25
七、对分支合并框架的几个问题归纳
1、影响ForkJoin加速效果的因素
1)并发数,即线程数。一般是可用的cpu数,默认就是这个(比如Runtime.getRuntime().availableProcessors()返回处理器的数量),一般表现很好。
2)任务切分的粒度。如果切分粒度等于总任务量,一个任务执行,就相当于单线程顺序执行。每个任务执行的计算量,太大的话加速效果有限,不能发挥到最好。相反,太小的话,消耗在任务管理的成本占了主要部分,导致还不如顺序执行的快。需要适当平衡二者,而总的执行时间还与任务的规模有关。
2、分治、并行、可伸缩的思考:
这三者关系很亲密。分治思想(divide-and-conquer)是一种简单朴素的思想,很多问题都可以这样解决。ForkJoin就相当于分治法的并行版本。 分治本身只是解决问题的思想,既可以顺序执行也可以并行执行,但是在并行环境中更加有效,因为可以并行处理子问题。而在并行方面,可并行处理问题要么是彼此完全独立的问题,要么是可分解单独处理的问题。可伸缩性又和能否并行处理紧密相关,因为如果不能并行处理就要受到单机处理能力的限制,也就难以伸缩了。
3、ForkJoin与MapReduce两个并行计算框架的区别?
MapReduce是把大数据集切分成小数据集,并行分布计算后再合并;ForkJoin是将一个问题递归分解成子问题,再将子问题并行运算后合并结果。
二者共同点:都是用于执行并行任务的。基本思想都是把问题分解为一个个子问题分别计算,再合并结果。应该说并行计算都是这种思想,彼此独立的或可分解的。从名字上看Fork和Map都有切分的意思,Join和Reduce都有合并的意思,比较类似。
区别:1)环境差异,分布式 vs 单机多核:ForkJoin设计初衷针对单机多核(处理器数量很多的情况)。MapReduce一开始就明确是针对很多机器组成的集群环境的。也就是说一个是想充分利用多处理器,而另一个是想充分利用很多机器做分布式计算。这是两种不同的的应用场景,有很多差异,因此在细的编程模式方面有很多不同。2)编程差异:MapReduce一般是:做较大粒度的切分,一开始就先切分好任务然后再执行,并且彼此间在最后合并之前不需要通信。这样可伸缩性更好,适合解决巨大的问题,但限制也更多。ForkJoin可以是较小粒度的切分,任务自己知道该如何切分自己,递归地切分到一组合适大小的子任务来执行,因为是一个JVM内,所以彼此间通信是很容易的,更像是传统编程方式。
4、ForkJoinPool与一般的ExecutorService实现的差别?
ForkJoin实现了ExecutorService接口,这个接口就是用来把任务交给线程池中的工作线程去执行。ForkJoin也是一个ExecutorService,但区别在于ForkJoin使用了work-stealing算法,普通的线程池是按FIFO的方式执行,而ForkJoin优先执行(由其它任务)后创建的子任务。对于大部分会产生子任务的任务模式,ForkJoin的处理实现会很高效。如果设置了异步模式, ForkJoin也可能适合执行事件类型(不需要join)的任务。另外,ForkJoinPool需要在程序结束时显式的停止,因为其中所有的线程都处于守护模式。
相关文章推荐
- fork-join框架
- Java线程之fork/join框架
- java 7 forkjoin并行框架的源码详究
- Java Fork-Join框架实现并发查找
- ForkJoin框架的一些原理知识点
- 并发编程之 Fork-Join 分而治之框架
- Fork/Join框架介绍 II 【在文档中查找一个词并返回文档或行中所出现这个词的次数】
- Fork/Join-Java并行计算框架
- [转]Java7中的ForkJoin并发框架初探(中)——JDK中实现简要分析
- 多线程(五) Fork/Join框架介绍及实例讲解
- Java7中的ForkJoin并发框架初探(中)——JDK中实现简要分析
- Fork/Join框架之Fork、Join操作
- Java线程篇(十一):Fork/Join-Java并行计算框架
- 并发编程之 Fork-Join 分而治之框架
- Java并发编程-Fork/Join框架同步与异常
- Java 7 Fork/Join 框架
- fork/join框架简介
- 第五章 Fork/Join Framework(Fork/join 框架)【上】
- Fork/Join框架使用
- Java多线程(11) Fork-Join框架