并行编程框架 ForkJoin

本文假设您已经了解一般并行编程知识,了解Java concurrent部分如ExecutorService等相关内容。

虽说是Java的ForkJoin并行框架,但不要太在意Java,其中的思想在其它语言环境也是同样适用的。因为并发编程在本质上是一样的。就好像如何找到优秀的Ruby程序员?其实要找的只是一个优秀的程序员。当然,如果语言层面直接支持相关的语义会更好。

引言

Java 语言从一开始就支持线程和并发性语义。Java5增加的并发工具又解决了一般应用程序的并发需求,Java6、Java7又进一步补充了一些内容。原来的工具主要是粗粒度的并发。比如每个web请求由一个工作线程处理,在线程池分配任务。而Java7中新引入的ForkJoin可以处理更细粒度的并行计算。

早期的时候都是单核cpu环境,如果不是多核环境下,线程/进程并不是真正的并行执行,主要用来表示异步执行效果。单核cpu上,假如每个任务完全是cpu密集的(没有等待),那么这种伪并发并不会使计算变快。只有在真正的多核环境才能起到加速作用,而现在多核已经普及,甚至已经到了手机上!

介绍

ForkJoin是适用于多核环境的轻量级并行框架。目标是在多核系统下,通过并行运算,充分利用多处理器,提高效率与加速运行。

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

ForkJoin由一组工作线程组成,用来执行任务,核心是work-stealing算法。可以有大量任务,但实际只有少量真正的物理线程,默认是机器的cpu数量,也可指定。很多其它工作的算法都可以在此基础之上进行。

虽然起初直接使用它的人可能不多,但将来会被很多框架在底层使用,因为是如此基础,所以最终ForkJoin可能会无处不在。

一般而言,使用者只需要关心两个方法fork() 和 join()。它们分别表示:子任务的异步执行和阻塞等待结果完成。

ForkJoin框架的核心是ForkJoinPool类,实现了work-stealing算法,用于执行ForkJoinTask类型的任务(也就是按照该算法调度线程与任务,当然还负责解决好相关的一些其它问题)。

work-stealing算法

work-stealing 是一种任务调度方法,由多个工作线程组成,每个工作线程用一个双端队列维护一组任务。Fork的时候是把任务加到队列的头部,而不像一般的线程池那样是加到任务队列末尾。工作线程选择头部最新的任务来执行。当工作线程没有任务可执行时,它会尝试从其它线程的任务队列尾部窃取一个任务执行。如果没有任务执行了并且窃取其它任务失败,那么工作线程停止。

这种方法的优点是减少了争用,因为工作线程从头获取任务,而窃取线程从尾部窃取任务。另一个优点是递归的分治法使得早期产生的是较大的任务单元,而窃取到较大任务会进一步递归分解,因此也减少了尾部窃取的次数。另外,父任务很可能要等待子任务(join),所以从队列头部子任务开始执行也是一种优化。

总之,它会使用有限的线程执行大量任务,同时保持各线程的任务都处于繁忙的执行状态,而尽量不让线程处于等待状态。为了做到这点可能会从其它线程的任务队列中窃取任务来执行,所以叫work-stealing。

就像前面所说物理线程不能太多,过多的话切换管理开销就会较大,还会消耗更多的内存等资源,并且没有带来任何好处。默认是用cpu数量的线程数,一般情况都比较合适(比如Runtime.getRuntime().availableProcessors()返回处理器的数量),但具体的数值还和任务自身的特点有关,可以通过不同参数测试比较一下。而任务可以是大量的,由每个线程的工作队列维护。

ForkJoin是简化了一些开发者的工作,如果不用ForkJoin,最原始的方式是自己手工切分任务并分别创建线程执行。

分治、并行、可伸缩的思考:

这三者关系很亲密。分治思想(divide-and-conquer)是一种简单朴素的思想,很多问题都可以这样解决。ForkJoin就相当于分治法的并行版本。 分治本身只是解决问题的思想,既可以顺序执行也可以并行执行,但是在并行环境中更加有效,因为可以并行处理子问题。而在并行方面,可并行处理问题要么是彼此完全独立的问题,要么是可分解单独处理的问题。可伸缩性又和能否并行处理紧密相关,因为如果不能并行处理就要受到单机处理能力的限制,也就难以伸缩了。

ForkJoin与MapReduce两个并行计算框架的区别 ?

MapReduce是把大数据集切分成小数据集,并行分布计算后再合并。

ForkJoin是将一个问题递归分解成子问题,再将子问题并行运算后合并结果。

二者共同点:都是用于执行并行任务的。基本思想都是把问题分解为一个个子问题分别计算,再合并结果。应该说并行计算都是这种思想,彼此独立的或可分解的。从名字上看Fork和Map都有切分的意思,Join和Reduce都有合并的意思,比较类似。

区别:

1)环境差异,分布式 vs 单机多核:ForkJoin设计初衷针对单机多核(处理器数量很多的情况)。MapReduce一开始就明确是针对很多机器组成的集群环境的。也就是说一个是想充分利用多处理器,而另一个是想充分利用很多机器做分布式计算。这是两种不同的的应用场景,有很多差异,因此在细的编程模式方面有很多不同。

2)编程差异:MapReduce一般是:做较大粒度的切分,一开始就先切分好任务然后再执行,并且彼此间在最后合并之前不需要通信。这样可伸缩性更好,适合解决巨大的问题,但限制也更多。ForkJoin可以是较小粒度的切分,任务自己知道该如何切分自己,递归地切分到一组合适大小的子任务来执行,因为是一个JVM内,所以彼此间通信是很容易的,更像是传统编程方式。

ForkJoin框架基本结构

ForkJoinPool本身实现了ExecutorService接口,负责调度执行ForkJoinTask。

ForkJoinTask是提交给ForkJoinPool 执行的任务,本身也实现了Future 接口。

ForkJoinTask有两个子类RecursiveAction和RecursiveTask。 RecursiveAction 没有返回值(只需fork);RecursiveTask有返回值(需要合并)。类似于Runnable和 Callable一样。没有返回值一般意味着所有子任务都执行完了即可,中间的子任务不需要join了。其实要不要返回值都可以实现,有返回值可以直接合并,没有返回值可以把结果保存在共享的数据上。

而我们要做的是实现自己要完成的任务,只需要继承其一,并覆盖抽象方法compute()。在这个方法中实现自己的任务,递归分解任务。

ForkJoinPool与一般的ExecutorService实现的差别

ForkJoin实现了ExecutorService接口,这个接口就是用来把任务交给线程池中的工作线程去执行。ForkJoin也是一个ExecutorService,但区别在于ForkJoin使用了work-stealing算法,见前面的介绍。普通的线程池是按FIFO的方式执行,而ForkJoin优先执行(由其它任务)后创建子任务。对于大部分会产生子任务的任务模式,ForkJoin的处理实现会很高效。如果设置了异步模式, ForkJoin也可能适合执行事件类型(不需要join)的任务。

影响ForkJoin加速效果的因素

理想效果是核越多加速效果越好。但是并行不一定更快,参数不对还可能更慢:

1)  并发数,即线程数。一般是可用的cpu数,默认就是这个,一般表现很好。

2)  任务切分的粒度。如果切分粒度等于总任务量,一个任务执行,就相当于单线程顺序执行。每个任务执行的计算量,太大的话加速效果有限,不能发挥到最好。相反,太小的话,消耗在任务管理的成本占了主要部分,导致还不如顺序执行的快。

需要适当平衡二者。因为还和任务本身的特定有关,所以可以做个基准测试比较一下。

而总的执行时间还与任务的规模有关。

任务粒度应该适中,多大合适?好像在什么地方上看到说:经验上单个任务为100-10000个基本指令,当然还和任务本身的特定有关。

个人感觉多核cpu只适用于解决计算密集型应用,因为实际问题可能IO等其他方面的瓶颈,多核也还是无法充分利用的。

使用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)  合并排序示例:

合并排序是常见的排序算法之一。示例实现了对一个整数数组的合并排序。同时还演示了不同并发数(线程数)与不同数组大小的组合测试。代码在<jdk_ home>/sample/ForkJoin/ 中。

2)  把图片模糊处理示例:

一个图片可以被表示为一个m*n大小的整数数组,其中每个整数表示一个像素(的颜色)。模糊处理之后的图像还是一个同样大小的整数数组。处理过程是把原来的每个像素与周围像素的颜色求平均值即可。如果顺序执行就是从头到尾对每个像素执行一次计算得到目标像素,因为每个像素的计算是独立的,所以可以把这个整数数组切分成一块一块的子数组(即子任务)分别执行。任务不适合切分的过小,所以设定了一个常数阈值10000,大小小于10000的子数组就直接执行,否则对半切分为两个子数组的任务分别执行。文章 源代码

在我自己的机器上i3处理器 (i3,4cpu),并行确实快了不少。

其它的例如:求最大值max、平均值avg、求和sum等聚合函数都是可以分解计算的。

示例中都是对数组的处理,比较常见的是对数组、集合进行并行地处理操作,但也不限于此。

网上有一些Fibonacci 的示例,但这些示例并不适合展示ForkJoin。

Doug Lea与JSR-166

说到Java并发编程,就不能不说到Doug Lea与JSR-166。Doug
Lea是并发编程方面的专家,纽约州立大学奥斯威戈分校的计算机教授。曾是JCP执行委员,是JSR-166的leader。JSR-166就是负责向Java语言中添加并发编程工具的,即我们见到的java.util.concurrent包(及子包)。还是《 Concurrent
Programming in Java Design Principles and Patterns》一书的作者,是这方面最早的书。他还是知名的内存分配方法dlmalloc的作者,这是C语言中的动态内存分配函数malloc的一种普遍使用的实现。

参考资料:

其它并行框架:

并行编程框架 ForkJoin

时间: 2024-11-05 06:25:21

并行编程框架 ForkJoin的相关文章

并行编程入门

目录 1. 并行编程简介 2. MapReduce 2.1 MapReduce简介 2.2 MapReduce框架 2.3 Hadoop介绍 2.4 Hadoop基本类 2.5 Hadoop编程实例 1.并行编程简介 1.1.并行编程作用,用途 商业用途,科学计算,大数据分析 1.2.并行编程兴起原因 目前的串行编程的局限性 使用的流水线等隐式并行模式的局限性 硬件的发展 1.3.并行算法设计原则步骤 a.分析问题 b.分解问题 其中分解方法有: 数据分解 递归分解 探测性分解 推测性分解 混合

Delphi xe7并行编程快速入门(转)

http://blog.csdn.net/henreash/article/details/41315183 现在多数设备.计算机都有多个CPU单元,即使是手机也是多核的.但要在开发中使用多核的优势,却需要一些技巧,花费时间编写额外的代码.好了,现在可以使用Delphi做并行编程了. 在Delphi.C++ Builder和RAD Studio XE7中,有一个简化并行运行任务的库,叫做并行编程库. 并行编程库在System.Threading单元中,其中提供了很多有用的特性,可方便的应用在已有

[转载]:Delphi xe7并行编程快速入门

现在多数设备.计算机都有多个CPU单元,即使是手机也是多核的.但要在开发中使用多核的优势,却需要一些技巧,花费时间编写额外的代码.好了,现在可以使用Delphi做并行编程了. 在Delphi.C++ Builder和RAD Studio XE7中,有一个简化并行运行任务的库,叫做并行编程库. 并行编程库在System.Threading单元中,其中提供了很多有用的特性,可方便的应用在已有项目和新项目中.提供了大量便利的重载函数,可同时支持C++和Object Pascal. 这些特性包括易用的针

Delphi xe7并行编程快速入门(三篇)

现在多数设备.计算机都有多个CPU单元,即使是手机也是多核的.但要在开发中使用多核的优势,却需要一些技巧,花费时间编写额外的代码.好了,现在可以使用Delphi做并行编程了. 在Delphi.C++ Builder和RAD Studio XE7中,有一个简化并行运行任务的库,叫做并行编程库. 并行编程库在System.Threading单元中,其中提供了很多有用的特性,可方便的应用在已有项目和新项目中.提供了大量便利的重载函数,可同时支持C++和Object Pascal. 这些特性包括易用的针

高大上函数响应式编程框架ReactiveCocoa学习笔记1 简介

原创文章,转载请声明出处哈. ReactiveCocoa函数响应式编程 一.简介 ReactiveCocoa(其简称为RAC)是函数响应式编程框架.RAC具有函数式编程和响应式编程的特性.它主要吸取了.Net的 Reactive Extensions的设计和实现. 函数式编程 (Functional Programming) 函数式编程也可以写N篇,它是完全不同于OO的编程模式,这里主要讲一下这个框架使用到的函数式思想. 1) 高阶函数:在函数式编程中,把函数当参数来回传递,而这个,说成术语,我

C#并行编程-PLINQ:声明式数据并行

原文:C#并行编程-PLINQ:声明式数据并行 背景 通过LINQ可以方便的查询并处理不同的数据源,使用Parallel LINQ (PLINQ)来充分获得并行化所带来的优势. PLINQ不仅实现了完整的LINQ操作符,而且还添加了一些用于执行并行的操作符,与对应的LINQ相比,通过PLINQ可以获得明显的加速,但是具体的加速效果还要取决于具体的场景,不过在并行化的情况下一段会加速. 如果一个查询涉及到大量的计算和内存密集型操作,而且顺序并不重要,那么加速会非常明显,然而,如果顺序很重要,那么加

C#中的多线程 - 并行编程 z

原文:http://www.albahari.com/threading/part5.aspx 专题:C#中的多线程 1并行编程Permalink 在这一部分,我们讨论 Framework 4.0 加入的多线程 API,它们可以充分利用多核处理器. 并行 LINQ(Parallel LINQ)或称为 PLINQ Parallel类 任务并行(task parallelism)构造 SpinLock 和 SpinWait 这些 API 可以统称为 PFX(Parallel Framework,并行

C#并行编程--命令式数据并行(Parallel.Invoke)

命令式数据并行   Visual C# 2010和.NETFramework4.0提供了很多令人激动的新特性,这些特性是为应对多核处理器和多处理器的复杂性设计的.然而,因为他们包括了完整的新的特性,开发人员和架构师必须学习一种新的编程模型. 这一章是一些新的类.结构体和枚举类型,你可以使用这里来处理数据并行的场景.这章将为你展示怎样创建并行代码和描述与每个场景相关的新概念,而不是关注并发编程中的最复杂的问题.这样你将可以更加充分的理解性能改进. 开始并行任务  使用先前版本的.NET Frame

并行计算复习————第四篇 并行计算软件支撑:并行编程

并行计算复习 第四篇 并行计算软件支撑:并行编程 Ch13 并行程序设计基础 13.1并行语言构造方法 库例程:MPI.Pthreads 扩展串行语言:Fortran90 加编译注释构造:OpenMP 13.2并行性问题 可利用SPMD来伪造MPMD 需要运行MPMD:parbegin S1 S2 S3 parend 可以改造成SPMD: for i = 1 to 3 par-do if i == 1 then S1 else if i == 2 then S2 else if i == 3 t