应用程序并行计算遇到的问题

当硬件处理能力不能按摩尔定律垂直发展的时候,选择了水平发展。多核处理器已广泛应用,未来处理器的核心数将进一步发布,甚至达到上百上千的数量。而现在很多的应用程序在运行在多核心的处理器上并不能得到很好的性能提升,因为应用程序的并发处理能力不强,不能够合理有效地的利用计算资源。线性的计算只能利用n分之一的计算支援。

要提高应用程序在多核处理器上的执行效率,只能想办法提高应用程序的本身的并行能力。常规的做法就是使用多线程,让更多的任务同时处理,或者让一部分操作异步执行,这种简单的多线程处理方式在处理器核心数比较少的情况下能够有效地利用处理资源,因为在处理器核心比较少的情况下,让不多的几个任务并行执行即可。但是当处理器核心数发展很大的数目,上百上千的时候,这种按任务的并发处理方法也不能充分利用处理资源,因为一般的应用程序没有那么多的并发处理任务(服务器程序是个例外)。所以,只能考虑把一个任务拆分为多个单元,每个单元分别得执行最后合并每个单元的结果。一个任务的并行拆分,一种方法就是寄希望于硬件平台或者操作系统,但是目前这个领域还没有很好的结果。另一种方案就是还是只有依靠应用程序本身对任务经行拆封执行。

Fork/Join框架

依靠应用程序本身并行拆封任务,如果使用简单的多线程程序的方法,复杂度必然很大。这就需要一个更好的范式或者工具来代程序员处理这类问题。Java 7也意识到了这个问题,才标准库中集成了由Doug Lea开发的Fork/Join并行计算框架。通过使用 Fork/Join 模式,软件开发人员能够方便地利用多核平台的计算能力。尽管还没有做到对软件开发人员完全透明,Fork/Join 模式已经极大地简化了编写并发程序的琐碎工作。对于符合 Fork/Join 模式的应用,软件开发人员不再需要处理各种并行相关事务,例如同步、通信等,以难以调试而闻名的死锁和 data race 等错误也就不会出现,提升了思考问题的层次。你可以把 Fork/Join 模式看作并行版本的 Divide and Conquer 策略,仅仅关注如何划分任务和组合中间结果,将剩下的事情丢给 Fork/Join 框架。但是Fork/Join并行计算框架,并不是银弹,并不能解决所有应用程序在超多核心处理器上的并发问题。

如果一个应用能被分解成多个子任务,并且组合多个子任务的结果就能够获得最终的答案,那么这个应用就适合用 Fork/Join 模式来解决。其原理如下图。

应用程序开发者需要做的就是拆分任务并组合每个子任务的中间结果,而不用再考虑线程和锁的问题。

二、工作窃取算法

指的是某个线程从其他队列里窃取任务来执行。使用的场景是一个大任务拆分成多个小任务,为了减少线程间的竞争,把这些子任务分别放到不同的队列中,并且每个队列都有单独的线程来执行队列里的任务,线程和队列一一对应。但是会出现这样一种情况:A线程处理完了自己队列的任务,B线程的队列里还有很多任务要处理。A是一个很热情的线程,想过去帮忙,但是如果两个线程访问同一个队列,会产生竞争,所以A想了一个办法,从双端队列的尾部拿任务执行。而B线程永远是从双端队列的头部拿任务执行(任务是一个个独立的小任务),这样感觉A线程像是小偷在窃取B线程的东西一样。

工作窃取算法的优点:

         利用了线程进行并行计算,减少了线程间的竞争。

工作窃取算法的缺点:

1、如果双端队列中只有一个任务时,线程间会存在竞争。

2、窃取算法消耗了更多的系统资源,如会创建多个线程和多个双端队列。

三、框架设计

Fork/Join中两个重要的类:

1、ForkJoinTask:使用该框架,需要创建一个ForkJoin任务,它提供在任务中执行fork和join操作的机制。一般情况下,我们并不需要直接继承ForkJoinTask类,只需要继承它的子类,它的子类有两个:

a、RecursiveAction:用于没有返回结果的任务。

b、RecursiveTask:用于有返回结果的任务。

2、ForkJoinPool:任务ForkJoinTask需要通过ForkJoinPool来执行。

 1 package test;
2
3 import java.util.concurrent.ExecutionException;
4 import java.util.concurrent.ForkJoinPool;
5 import java.util.concurrent.Future;
6 import java.util.concurrent.RecursiveTask;
7
8
9 public class CountTask extends RecursiveTask<Integer>
10 {
11 private static final long serialVersionUID = 1L;
12 //阈值
13 private static final int THRESHOLD = 2;
14 private int start;
15 private int end;
16
17 public CountTask(int start, int end)
18 {
19 this.start = start;
20 this.end = end;
21 }
22
23 @Override
24 protected Integer compute()
25 {
26 int sum = 0;
27 //判断任务是否足够小
28 boolean canCompute = (end - start) <= THRESHOLD;
29 if(canCompute)
30 {
31 //如果小于阈值,就进行运算
32 for(int i=start; i<=end; i++)
33 {
34 sum += i;
35 }
36 }
37 else
38 {
39 //如果大于阈值,就再进行任务拆分
40 int middle = (start + end)/2;
41 CountTask leftTask = new CountTask(start,middle);
42 CountTask rightTask = new CountTask(middle+1,end);
43 //执行子任务
44 leftTask.fork();
45 rightTask.fork();
46 //等待子任务执行完,并得到执行结果
47 int leftResult = leftTask.join();
48 int rightResult = rightTask.join();
49 //合并子任务
50 sum = leftResult + rightResult;
51
52 }
53 return sum;
54 }
55
56 public static void main(String[] args)
57 {
58 ForkJoinPool forkJoinPool = new ForkJoinPool();
59 CountTask task = new CountTask(1,6);
60 //执行一个任务
61 Future<Integer> result = forkJoinPool.submit(task);
62 try
63 {
64 System.out.println(result.get());
65 }
66 catch (InterruptedException e)
67 {
68 e.printStackTrace();
69 }
70 catch (ExecutionException e)
71 {
72 e.printStackTrace();
73 }
74
75 }
76
77 }

这个程序是将1+2+3+4+5+6拆分成1+2;3+4;5+6三个部分进行子程序进行计算后合并。

一个简单的例子

我们首先看一个简单的Fork/Join的任务定义。

  1. public class Calculator extends RecursiveTask<Integer> {
  2. private static final int THRESHOLD = 100;
  3. private int start;
  4. private int end;
  5. public Calculator(int start, int end) {
  6. this.start = start;
  7. this.end = end;
  8. }
  9. @Override
  10. protected Integer compute() {
  11. int sum = 0;
  12. if((start - end) < THRESHOLD){
  13. for(int i = start; i< end;i++){
  14. sum += i;
  15. }
  16. }else{
  17. int middle = (start + end) /2;
  18. Calculator left = new Calculator(start, middle);
  19. Calculator right = new Calculator(middle + 1, end);
  20. left.fork();
  21. right.fork();
  22. sum = left.join() + right.join();
  23. }
  24. return sum;
  25. }
  26. }

这段代码中,定义了一个累加的任务,在compute方法中,判断当前的计算范围是否小于一个值,如果是则计算,如果没有,就把任务拆分为连个子任务,并合并连个子任务的中间结果。程序递归的完成了任务拆分和计算。

任务定义之后就是执行任务,Fork/Join提供一个和Executor框架 的扩展线程池来执行任务。

  1. @Test
  2. public void run() throws Exception{
  3. ForkJoinPool forkJoinPool = new ForkJoinPool();
  4. Future<Integer> result = forkJoinPool.submit(new Calculator(0, 10000));
  5. assertEquals(new Integer(49995000), result.get());
  6. }

Fork/Join框架的主要类

RecursiveAction供不需要返回值的任务继续。

RecursiveTask通过泛型参数设置计算的返回值类型。

ForkJoinPool提供了一系列的submit方法,计算任务。ForkJoinPool默认的线程数通过Runtime.availableProcessors()获得,因为在计算密集型的任务中,获得多于处理性核心数的线程并不能获得更多性能提升。

public <T> ForkJoinTask<T> submit(ForkJoinTask<T> task) {
    doSubmit(task);
    return task;
}

sumit方法返回了task本身,ForkJoinTask实现了Future接口,所以可以通过它等待获得结果。

另一例子

这个例子并行排序数组,不需要返回结果,所以继承了RecursiveAction。

  1. public class SortTask extends RecursiveAction {
  2. final long[] array;
  3. final int start;
  4. final int end;
  5. private int THRESHOLD = 100; //For demo only
  6. public SortTask(long[] array) {
  7. this.array = array;
  8. this.start = 0;
  9. this.end = array.length - 1;
  10. }
  11. public SortTask(long[] array, int start, int end) {
  12. this.array = array;
  13. this.start = start;
  14. this.end = end;
  15. }
  16. protected void compute() {
  17. if (end - start < THRESHOLD)
  18. sequentiallySort(array, start, end);
  19. else {
  20. int pivot = partition(array, start, end);
  21. new SortTask(array, start, pivot - 1).fork();
  22. new SortTask(array, pivot + 1, end).fork();
  23. }
  24. }
  25. private int partition(long[] array, int start, int end) {
  26. long x = array[end];
  27. int i = start - 1;
  28. for (int j = start; j < end; j++) {
  29. if (array[j] <= x) {
  30. i++;
  31. swap(array, i, j);
  32. }
  33. }
  34. swap(array, i + 1, end);
  35. return i + 1;
  36. }
  37. private void swap(long[] array, int i, int j) {
  38. if (i != j) {
  39. long temp = array[i];
  40. array[i] = array[j];
  41. array[j] = temp;
  42. }
  43. }
  44. private void sequentiallySort(long[] array, int lo, int hi) {
  45. Arrays.sort(array, lo, hi + 1);
  46. }
  47. }
  1. @Test
  2. public void run() throws InterruptedException {
  3. ForkJoinPool forkJoinPool = new ForkJoinPool();
  4. Random rnd = new Random();
  5. long[] array = new long[SIZE];
  6. for (int i = 0; i < SIZE; i++) {
  7. array[i] = rnd.nextInt();
  8. }
  9. forkJoinPool.submit(new SortTask(array));
  10. forkJoinPool.shutdown();
  11. forkJoinPool.awaitTermination(1000, TimeUnit.SECONDS);
  12. for (int i = 1; i < SIZE; i++) {
  13. assertTrue(array[i - 1] < array[i]);
  14. }
  15. }

动手尝试

Fork/Join框架的代码已经整合到了最新的JDK7的Binary Snapshot Releases中,可以通过这个地址 下载。

本文中的代码见附件。

java Fork/Join框架的更多相关文章

  1. Java Fork/Join 框架

    简介 从JDK1.7开始,Java提供Fork/Join框架用于并行执行任务,它的思想就是讲一个大任务分割成若干小任务,最终汇总每个小任务的结果得到这个大任务的结果. 这种思想和MapReduce很像 ...

  2. JAVA中的Fork/Join框架

    看了下Java Tutorials中的fork/join章节,整理下. 什么是fork/join框架 fork/join框架是ExecutorService接口的一个实现,可以帮助开发人员充分利用多核 ...

  3. Java并发——Fork/Join框架

    为了防止无良网站的爬虫抓取文章,特此标识,转载请注明文章出处.LaplaceDemon/ShiJiaqi. http://www.cnblogs.com/shijiaqi1066/p/4631466. ...

  4. Java并发编程--Fork/Join框架使用

    上篇博客我们介绍了通过CyclicBarrier使线程同步,可是上述方法存在一个问题,那就是假设一个大任务跑了2个线程去完毕.假设线程2耗时比线程1多2倍.线程1完毕后必须等待线程2完毕.等待的过程线 ...

  5. Java 7 Fork/Join 框架

    在 Java7引入的诸多新特性中,Fork/Join 框架无疑是重要的一项.JSR166旨在标准化一个实质上可扩展的框架,以将并行计算的通用工具类组织成一个类似java.util中Collection ...

  6. 《java.util.concurrent 包源码阅读》22 Fork/Join框架的初体验

    JDK7引入了Fork/Join框架,所谓Fork/Join框架,个人解释:Fork分解任务成独立的子任务,用多线程去执行这些子任务,Join合并子任务的结果.这样就能使用多线程的方式来执行一个任务. ...

  7. Java并发——Fork/Join框架与ForkJoinPool

    为了防止无良网站的爬虫抓取文章,特此标识,转载请注明文章出处.LaplaceDemon/ShiJiaqi. http://www.cnblogs.com/shijiaqi1066/p/4631466. ...

  8. Fork/Join 框架-设计与实现(翻译自论文《A Java Fork/Join Framework》原作者 Doug Lea)

    作者简介 Dong Lea任职于纽约州立大学奥斯威戈分校(State University of New York at Oswego),他发布了第一个广泛使用的java collections框架实 ...

  9. Java开发笔记(一百零六)Fork+Join框架实现分而治之

    前面依次介绍了普通线程池和定时器线程池的用法,这两种线程池有个共同点,就是线程池的内部线程之间并无什么关联,然而某些情况下的各线程间存在着前因后果关系.譬如人口普查工作,大家都知道我国总人口为14亿左 ...

随机推荐

  1. 使用for in循环遍历json对象的数据

    使用for in遍历json对象数据,如果数据中的名称有为数字的话,只对正整数有效,那么先会输出为正整数的数据,后面其他的会按照原来数据中定义的顺序不变输出. 针对名称为数字的json对象数据进行测试 ...

  2. Unity3D中随机函数的应用

    电子游戏中玩家与系统进行互动的乐趣绝大多数取决于事件发生的不可预知性和随机性.在unity3D的API中提供了Random类来解决随机问题. 最简单的应用就是在数组中随机选择一个元素,使用Random ...

  3. .NET高级代码审计(第二课) Json.Net反序列化漏洞

    0X00 前言 Newtonsoft.Json,这是一个开源的Json.Net库,官方地址:https://www.newtonsoft.com/json ,一个读写Json效率非常高的.Net库,在 ...

  4. 《ASP.NET MVC 5 破境之道》:第一境 ASP.Net MVC5项目初探 — 第一节:运行第一个MVC5项目

    第一境 ASP.Net MVC5项目初探 — 第一节:运行第一个MVC5项目 创建一个MVC项目,是很容易的,大部分工作,VS都帮我们完成了.只需要按照如下步骤按部就班就可以了. 打开VS2017,选 ...

  5. day 69 ORM 多表增删改查操作

    http://www.cnblogs.com/liwenzhou/p/8660826.html 下面的代码是在 python console中配置的. 关闭pycharm会消失. from app01 ...

  6. docker 下载加速

    执行这个命令: curl -SSL https://get.daocloud.io/daotools/set_mirror.sh | sh -s http://f1361db2.m.daocloud. ...

  7. 虚拟安装centos后无法上网、DNS无法解析问题解决

    1.保证拟机ip和VMnet8的ip在同一网段内 2.虚拟机网关和VMnet8相同

  8. “全栈2019”Java多线程第二十章:同步方法产生死锁的例子

    难度 初级 学习时间 10分钟 适合人群 零基础 开发语言 Java 开发环境 JDK v11 IntelliJ IDEA v2018.3 文章原文链接 "全栈2019"Java多 ...

  9. 输出图中顶点i到顶点j之间的所有简单路径

    简单路径(不包括环) DFS遍历以及回溯得到结果 void dfs(ALGraph graph, int v, int end, bool visit[], int path[], int cnt) ...

  10. 【JS深入学习】—— 一句话解释闭包

    闭包的定义: 闭包(closuer)是一个受到保护的变量空间,由内嵌函数构成.就是说闭包内的变量不能被外部函数访问,为什么会这样? 函数的作用域: JS具有函数级的作用域,这表明外部函数不能访问内部函 ...