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

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

    获取年月日时分秒: package com.util; import java.text.DateFormat; import java.util.Calendar; import java.util ...

  2. linux系统编程之进程(六):父进程查询子进程的退出,wait,waitpid

    本节目标: 僵进程 SIGCHLD wait waitpid 一,僵尸进程 当一个子进程先于父进程结束运行时,它与其父进程之间的关联还会保持到父进程也正常地结束运行,或者父进程调用了wait才告终止. ...

  3. ASP.NET MVC Ajax 伪造请求

    1.前言 CSRF(Cross-site request forgery)跨站请求伪造,ASP.NET MVC 应用通过使用AJAX请求来提升用户体验,浏览器开发者工具可以一览众山小,就很容易伪造了请 ...

  4. C# winform 记住密码实现代码

    using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; usin ...

  5. C# 对接Https接口

    最近公司项目需要对接Https接口,将对接的代码整理如下: public void Get() { HttpWebRequest request = null; request = WebReques ...

  6. 利用ligerUI隐藏某列,并不产生空白列的方法

    var grid;//声明变量 $(function () { //grid初始化 grid = $("#maingrid4").ligerGrid({ columns: [ { ...

  7. .net core api Post请求

    POST请求: 1.请求类型:Content-Type:application/json,参数:json字符串 后台接收参数: 1)([FromBody]UserInfo user)必须以对象形式接收 ...

  8. openvswitch 驱动卸载失败(Module openvswitch is in use)

    现象: [root@ostack1 ~]# modprobe -r openvswitchmodprobe: FATAL: Module openvswitch is in use. 解决: [roo ...

  9. thinkphp5的mkdir() Permission denied问题

    最近一直在用tp5写项目,在此遇到的问题也比较多.今天来谈谈“mkdir() Permission denied”错误. 你如果不仅仅写代码,还得部署到线上,那么这个tp5的这个错误,你有很大概率会遇 ...

  10. QQ个人信息保护 | 攻的对面叫防

    近来我们愈来愈重视个人信息保护,当接到骚扰电话时,你心里或许在想(我手机号怎么又被别人知道的?别人是怎么知道我手机号的?),现在的时代,手机已或不可缺,QQ几乎每个人都在用.而有些人总想保护个人QQ资 ...