论文翻译 | 【深入挖掘 Java 技术】「底层原理专题」深入分析一下并发编程之父 Doug Lea 的纽约州立大学的 ForkJoin 框架的本质和原理
前提介绍
Doug Lea 在州立大学奥斯威戈分校(Doug Lea)
摘要
本文深入探讨了一个 Java 框架的设计、实现及其性能。该框架遵循并行编程的理念,通过递归方式将问题分解为多个子任务,并利用工作窃取技术进行并行处理。所有子任务完成后,其结果被整合以形成完整的并行程序。
在总体设计上,该框架借鉴了 Cilk 工作窃取框架的核心理念。其核心技术主要聚焦于高效的任务队列构建和管理,以及工作线程的管理。经过实际性能测试,我们发现大多数程序的并行加速效果显著,但仍有优化空间,未来可能需要进一步研究改进方案。
引言
Fork/Join 并行是一种简单而高效的设计技术。它的算法思想是分而治之算法的并行版本,其典型形式包括:首先将问题分解为两个或更多的子问题,然后对每个子问题进行独立求解,最后将各个子问题的解合并以形成最终的解决方案。
fork 操作会启动一个新的并行 fork/join 子任务。
join 连接操作会导致当前任务不继续执行,直到子任务完成。
fork/join 算法与其他一样,fork/join 算法几乎总是递归的、反复拆分子任务,直到它们小到可以用简单、简短的顺序方法解决为止。使用简单、简短的顺序方法。
FJTask 是支持这种编程风格的 JavaTM 框架。FJTask 作为 java.util.concurrent 包的一部分,可从 http://gee.cs.oswego.edu 获取。
设计
任何支持构建并行执行的子任务的框架来运行 fork/join 程序。支持构建并行执行的子任务、的框架运行。
不过,java.lang.Thread 类(以及 POSIX pthreads 通常是 Java 线程的基础)不是支持 fork/join 程序的最优的工具。
性能优秀
fork/join 任务的同步和管理要求相对简单和有规律。其产生的计算图允许采用不同于通用线程所需的调度策略。例如,除了等待子任务外,fork/join 任务从不需要阻塞。因此,通用线程的阻塞状态跟踪被视为一种资源浪费。
此外,fork/join 框架还可以利用工作窃取技术,将任务从繁忙的线程转移到空闲线程,进一步优化并行处理。
任务粒度合理
在基本任务粒度合理的情况下,构建和管理线程的成本可能高于任务本身的计算时间。虽然粒度可以在特定平台上运行程序时进行调整,但极粗粒度会限制利用并行性的机会。
简而言之,标准的线程框架过于复杂,无法满足大多数分叉/连接程序的需求。然而,线程作为其他类型并行和并行编程方式的基础,要仅仅为了支持这种编程风格而消除其开销或调整线程本身的调度是不可能的,或者至少是不切实际的。
Cilk 框架和基础
虽然这些想法肯定有更长的历史,但第一个为这些问题提供系统解决方案的编程框架是 Cilk。Cilk 和其他轻量级可执行框架是在操作系统的基本线程或进程机制之上的特殊目的的框架,支持 fork/join。
fork/join 的可移植性
这种策略同样适用于 Java,尽管 Java 线程又依赖于更低级别的操作系统功能。创建这样一个 Java 轻量级执行框架的主要优点是允许 fork/join 程序以更可移植的方式编写,并在各种支持 JVM 的系统上运行。
FJTask 框架
FJTask 框架是基于 Cilk 中使用的设 计的一个变体。其他变体存在于 Hood, Filaments,stackthreads,以及一些相关的轻量级系统中。
可执行任务。所有这些框架都将任务映射到线程,其方式与操作系统将线程映射到 CPU 相同,但在执行映射时,fork/join 框架利用了 fork/join 程序的简单性、规律性和约束。虽然所有这些框架都可以适应(在不同程度上)以不同风格编写的并行程序,但它们针对 fork/join 设计进行了优化。
设计思路
线程映射关系
已经建立了一个工作线程池。每个工作线程都是一个标准的(“重的”)线程(这里是线程子类 FJTaskRunner 的一个实例),它负责处理队列中保存的任务。通常,系统上的工作线程数量和 CPU 核心数一样多。在 Cilk 等本地框架中,这些线程被映射到内核线程或轻量级进程,然后再映射到 CPU。
在 Java 中,必须信任 JVM 和 OS 才能将这些线程映射到 CPU。然而,对于操作系统来说,这是一个相对简单的任务,因为这些线程是计算密集型的。任何合理的映射策略都会将这些线程映射到不同的 CPU 核心上。
拆分子任务
在 FJTask 框架中,所有的 fork/join 任务都是轻量级可执行类的实例,而不是线程的实例。这些任务子类化 FJTask,而不是线程,因为独立的可执行任务需要实现接口 Runnable 并定义一个 run 方法。
此外,这些任务都实现了 Runnable 接口,这使得它们可以作为正在执行的任务或线程的一部分交替运行。由于任务在 FJTask 方法支持的受限制的规则下操作,因此对 FJTask 进行子类化更加方便,以便能够直接调用它们。
排队及调度
在特殊目的的排队和调度规则下,任务通过工作线程得以执行和管理。这些机制通过任务类中的方法触发,主要包括 fork、join、完成状态指示器 isDone,以及一些实用的方法,如 coInvoke,即分叉并随后连接两个或多个任务。
设置调度管理
一个简单的控制和管理工具(这里是 FJTaskRunnerGroup)在从普通线程(如在 Java 程序中执行主任务的线程)调用时,设置工作池并启动给定的分叉/连接任务的执行。
标准示例
作为程序员如何看待这个框架的标准示例,这里是一个计算斐波那契函数的类。
这个版本的运行速度至少比在一个新的 java.lang 中运行的同等程序快 30 倍。它在维护多线程 Java 程序的内在可移植性的同时也做到了这一点。程序员典型感兴趣的调优参数:
在构建工作线程时,其数量通常应与平台上的可用 CPU 数量相匹配(或更少,以保留处理用于其他非相关目的),有时甚至可能更多,以吸收非计算任务。
一个粒度参数用于确定何时生成任务的成本超过了潜在的并行性带来的好处。这个参数更多地依赖于算法本身,而不是平台。通常,我们可以设定一个阈值,当在单处理器上运行时能获得良好的结果,但当存在多个 CPU 时仍能充分利用它们。这种方法的好处在于它与 JVM 的动态编译机制相契合,能够更优化地处理小方法。此外,数据局部性的优势也使得 fork/join 算法在某些情况下优于其他类型的算法。
未完待续
本节内容,给大家带来了对应的 fork/join 框架的前世今生,以及基于框架的 fork 和 join 机制的论文介绍,后续接下来会给大家带来对应的【线程盗取篇章】:论文翻译 | 【深入挖掘 Java 技术】「底层原理专题」深入分析一下并发编程之父 Doug Lea 的纽约州立大学的 ForkJoin 框架的本质和原理(线程盗取)
版权声明: 本文为 InfoQ 作者【洛神灬殇】的原创文章。
原文链接:【http://xie.infoq.cn/article/0df5665bf8ef6e2311db1d361】。文章转载请联系作者。
评论