教程– GPar,使并行系统对Groovy和Java友好

GPars是用于Groovy(和Java)中高级并发的开源库。 如果您曾经听说过诸如参与者,数据流或并行集合之类的术语,并且想以Java友好的语言尝试这些概念,那么现在您就有机会了。 在本文中,我打算为您提供GPar中可用抽象的快速概述。 然后,我们将更详细地研究基于流的并发数据处理,并行化收集操作以及异步运行组合函数。

随着多核芯片成为主流计算机,平板电脑和电话中的规范,并发编程变得越来越重要。 不幸的是,我们从Java中了解到的广泛的基于线程的并发模型与人脑的工作方式并不匹配。 线程和锁在代码中引入了过多的不确定性,这常常导致难以发现和修复的细微错误。 此类代码无法可靠地测试或分析。 为了使并发编程有效,我们不可避免地需要使用更自然的思维模型。

并发可以是Groovy

将直观的并发模型带入主流是GPar想要解决的挑战。 我们采用了著名的并发概念,例如参与者,CSP,数据流等,并在Java中实现了它们,并使用了Groovy DSL美味的顶部,使该库更加流畅且易于使用。 尽管主要针对Groovy,但某些GPar抽象可以直接从Java使用。 感谢Groovy社区对并发的兴趣以及对项目的支持,GPars当前是Groovy发行版的标准部分。 在Groovy中并发启动并运行不需要任何其他设置。

被认为是致命的循环

我希望您停一会儿,想一想学习编程的计算机科学专业学生通常进行的琐碎练习。 例如,这样的任务之一就是找到一个集合的最大值。 可能会应用一些复杂的度量,以使解决方案的计算量更大。 首先想到什么算法?

您很可能会提出一个迭代,该迭代将遍历整个集合并记住迄今为止找到的最大元素。 一旦我们到达集合的末尾,我们至今所记得的最大元素必须是整个集合的全局最大值。 清晰,简单,而且-错了! 如果您想找出原因,请继续阅读。

做出你的选择

在并发空间中似乎没有单一的“一刀切”的解决方案。 逐渐出现了多种范例,尽管有些重叠,但每种范例都适用于不同类型的问题。 GPars本身始于2008年,最初是一个用于并行收集处理的小型Groovy库。 此后不久便增加了对actor模型的支持。 随着时间的流逝,其他并发抽象已被集成。 以下是GPars 0.12版中当前可用内容的快速列表:

  • 并行集合提供了直观的方式来并行处理Java / Groovy集合,地图和一般所有几何可分解问题的处理

  • 异步功能使Groovy闭包可以异步运行,同时可以毫不费力地协调它们之间的相互通信。

  • Fork / Join使您能够同时处理递归的分治式算法

  • 数据流变量(又名Promises)为线程间通信提供了一种轻量级的机制。

  • 数据流通道和运算符使您可以将活动的数据转换元素组织到高度并发的数据处理管道和网络中

  • CSP是基于理论数学的著名并发模型 ,它使用通过同步通道进行通信的独立并发运行流程的抽象

  • Actor / Active对象为您提供了低礼仪事件驱动的主动组件的抽象,这些组件异步交换数据

  • 顾名思义,代理可以保护多个线程需要同时访问的数据

您可以在GPars用户指南中查看每个模型的详细信息,并与它们的典型适用范围并排比较。 另外,DierkKönig即将出版的第二版“ Groovy in Action” ,涵盖了GPar的详尽细节。

在本文中,我选择了三种最有可能向您展示直观并发优势的抽象-并行集合,异步函数和数据流运算符。 让我们潜入吧!

几何分解

现在,这是一个很好的地方,以解释为什么我们前面所述的用于找到最大值的顺序算法是一个错误的选择。 并非解决方案不正确。 显然,它可以为您提供有效的答案,不是吗? 它失败的地方是它的有效性。 它禁止随着工人人数的增加而扩大规模。 它完全忽略了系统可能会在问题上放置多个处理器的可能性。

几十年前,超市解决了这一挑战。 当收银台的队列过长时,他们会召集额外的收银员为客户提供服务,因此工作量得到了分配,吞吐量增加了。

回到我们找到最大值的问题:利用Groovy功能集合API,GPars添加了每个流行的迭代方法的并行版本,例如 eachParallel() collectParallel() findAllParallel() maxParallel() 等。 这些方法向用户隐藏了实际的实现。 在幕后,将集合分成较小的块,可能按层次进行组织,并且每个块将由不同的线程处理( 图1 )。

实际工作由用户必须提供的线程池中的线程执行。 GPar带有两种类型的线程池:

  • GParsExecutorsPool 使用直接的Java 5 Executors

  • GParsPool 使用Fork / Join线程池

图1:几何分解

Parallel collections in use:
GParsPool.withPool 16, {
    def myFavorite = programmingLanguages.collectParallel {it.grabSpec()}
                        .maxParallel {it.spec.count(/parallel/)}
}

withPool 代码块中,并行收集方法自动在周围线程池的线程之间分配工作。 您添加到池中的线程越多,获得的并行度就越高。 没有明确的池大小要求,该池将为运行时检测到的每个可用处理器创建一个线程,从而为您提供最大的计算能力。 这样,您的算法中就不会存在人为的上限来限制并行性。 无论是在旧的单处理器计算机上还是在未来的百核芯片上运行,代码都将全速运行。

GPars并行集合API提供了通常称为 “循环并行”问题 或更笼统的“ 几何分解”问题的解决方案 还有其他类型的挑战,需要采用更具创造性的并发方法。 我们将在本文的下一部分中讨论其中两个。


看到并发处理集合之后,我们现在将重点介绍函数。 Groovy对函数式编程提供了很好的支持。 毕竟,能够并行化方法和函数调用肯定会派上用场。 为了靠近Groovy所在的领域,我为下一个旅程选择了软件项目构建编排的问题。

注意: 当并行化构建过程时,通常I / O绑定比CPU绑定更多,显然,我们在增加磁盘和网络带宽的利用率,而没有增加处理器。 它不仅是CPU,而且还有其他资源,利用并发代码可以提高其利用率。 显然,所证明的原理可以完全相同的方式应用于CPU约束问题。

为了进行演示,我们假设我们具有一组功能,可能以壳脚本,gradle任务或GAnt方法实现,它们可以执行构建的不同部分。 然后,传统的构建脚本可能类似于以下所示:

println "Starting the build process."
def projectRoot = checkout('git@github.com:vaclav/GPars.git')
def classes = compileSources(projectRoot)
def api = generateAPIDoc(projectRoot)
def guide = generateUserDocumentation(projectRoot)
def result = deploy(packageProject(classes, api, guide))
使其平行

现在,我们的任务是尽可能多地安全地并行化内部版本。 您可能会看到 compileSources() generateAPIDoc() generateUserGuide() 可以安全地并行运行,因为它们没有相互依赖性。 他们只需要等待 checkout() 完成才可以开始工作。 但是,该脚本按顺序运行它们。

我敢肯定,您可以想象比这复杂得多的构建方案。 但是,如果没有一个很好的抽象,如果我们的任务是同时运行构建任务,即使使用这种人为简化的构建脚本,我们也要进行大量工作(清单2)。 好吧,您对此有何看法?

清单2:使用异步函数的并发版本的构建脚本

withPool {
    /* We need asynchronous variants of all the individual build steps */
    def aCheckout = checkout.asyncFun()
    def aCompileSources = compileSources.asyncFun()
    def aGenerateAPIDoc = generateAPIDoc.asyncFun()
    def aGenerateUserDocumentation = generateUserDocumentation.asyncFun()
    def aPackageProject = packageProject.asyncFun()
    def aDeploy = deploy.asyncFun()

    /* Here's the composition of asynchronous build steps to form a process */
    Promise projectRoot = aCheckout('git@github.com:vaclav/GPars.git')
    Promise classes = aCompileSources(projectRoot)
    Promise api = aGenerateAPIDoc(projectRoot)
    Promise guide = aGenerateUserDocumentation(projectRoot)
    Promise result = aDeploy(aPackageProject(classes, api, guide))

    /* Now we're setup and can wait for the build to finish */
    println "Starting the build process. This line is quite likely to be printed first ..."
    println result.get()
}

接线完全相同。 我们仅通过 asyncFun() 方法 将原始函数转换为异步函数 而且,整个代码块现在都包装在 GParsPool.withPool() 块中,因此这些函数具有一些可用于其辛苦工作的线程。

显然, asyncFun() 方法中 发生了许多不可思议的事情, 它们 允许这些函数异步运行,但在它们彼此需要数据时进行协作。

本质上, asyncFun() 将原始函数包装在一个新函数中。 新功能的签名与原始功能的签名略有不同。 例如,当原始的 compileSources() 函数将 String 作为参数并返回 String 作为结果时:

String compileSources = {String projectRoot -> ...}

新构建的 aCompileSources() 函数返回 字符串 ,而不是 字符串 本身 无极 同样, String Promise <String> 都被接受为参数:

Promise<String> aCompileSources = {String | Promise<String> projectRoot -> ...}

应遵守的承诺

Promise接口是几个GPars API的基本基石。 它有点类似于 java.util.concurrent.Future ,因为它表示正在进行的异步活动,并且可以通过 阻塞 get() 方法 来等待并获得结果 两者之间最重要的区别是 Promises 与Java的 Futures 不同 ,它允许非阻塞读取。

promise.then {println "Now we have a result: $it"}

这使得我们的异步函数仅在其计算所需的所有值均可用时才消耗系统线程。 因此,例如,打包将仅在 class api guide 局部变量都绑定到结果值之后开始。 在此之前, aPackage() 函数在后台排定的非活动 且未 激活的状态下静默等待( 图2 )。

图2:功能

崔波诺

现在,您应该能够看到构建块可以很好地组合在一起。 由于异步函数返回并接受promise,因此可以用与同步原始函数相同的方式来组成它们。 功能组合的第二个好处,也许是更为突出的好处是,我们不必明确指定哪些任务可以并行运行。 我敢肯定,如果我们继续在构建过程中添加任务,那么很快就会失去对什么活动可以安全地与其他活动并行运行的全局了解。 幸运的是,我们的异步函数将在运行时自行发现并行性。 任何时候,所有已准备好参数的任务都会从分配的线程池中获取线程并开始运行。 通过限制池中的线程数,可以设置并行运行的任务数的上限。

数据流向何处

现在,我们已经准备好进行当今设置中最有趣的抽象- 数据流并发 在前面的示例的基础上,我们现在 继续进行 我们将重复运行构建脚本,以构建多个项目。 如果愿意,可以将其视为将来构建服务器的初始阶段。 在系统资源允许的情况下,各种项目的构建请求将通过管道进入,我们的构建服务器将依次处理它们。

您可能会想尝试最简单的解决方案–对每个传入请求运行上一个练习的基于异步函数的代码。 但是,很有可能这将是次优的。 将多个请求堆叠在请求队列中,我们就有机会获得更大的并行度。 不仅可以并行构建同一项目的独立部分,而且还可以同时处理不同项目的不同部分。 简而言之,不同项目的处理可能会在时间上重叠( 图3 )。

图3:重叠

顺其自然

自然而然地适合此类问题的模型称为 数据流网络 它通常用于并发数据处理,例如加密和压缩, 数据挖掘 ,图像处理等。 本质上,数据流网络由通过异步通道连接的活动数据转换元素(称为运算符)组成。 每个操作员都从其输入通道使用数据,并通过多个输出通道发布结果。 它还具有关联的转换功能,该功能将通过输入通道接收的数据转换为数据以向下发送输出通道。 在幕后,操作员共享一个线程池,因此没有要处理的数据的不活动操作员不会占用系统线程。 对于我们简单的构建服务器,网络可能 如图4所示

图4:运营商网络选项1

我们每个步骤都有一个运算符。 通道表示构建任务之间的依赖关系,每个操作员将只需要系统线程并在其所有输入通道都具有要读取的值之后启动计算。

清单3:使用数据流运算符的并发构建服务器
/* We need channels to wire active elements together */
def urls = new DataflowQueue()
def checkedOutProjects = new DataflowBroadcast()
def compiledProjects = new DataflowQueue()
def apiDocs = new DataflowQueue()
def userDocs = new DataflowQueue()
def packages = new DataflowQueue()
def done = new DataflowQueue()

/* Here's the composition of individual build steps into a process */
operator(inputs: [urls], outputs: [checkedOutProjects], maxForks: 3) {url ->
    bindAllOutputs checkout(url)
}
operator([checkedOutProjects.createReadChannel()],
         [compiledProjects]) {projectRoot ->
    bindOutput compileSources(projectRoot)
}
operator(checkedOutProjects.createReadChannel(), apiDocs) {projectRoot ->
    bindOutput generateAPIDoc(projectRoot)
}
operator(checkedOutProjects.createReadChannel(), userDocs) {projectRoot ->
    bindOutput generateUserDocumentation(projectRoot)
}
operator([compiledProjects, apiDocs, userDocs], 
        [packages]) {classes, api, guide ->
    bindOutput packageProject(classes, api, guide)
}
def deployer = operator(packages, done) {packagedProject ->
    if (deploy(packagedProject) == 'success') bindOutput true
    else bindOutput false
}

/* Now we're setup and can wait for the build to finish */
println "Starting the build process. This line is quite likely to be printed first ..."
deployer.join()  //Wait for the last operator in the network to finish

此模型应用于我们的问题的好处是,例如,当第一个操作员执行项目结帐并完成获取项目的源时,它可以立即获取队列中的下一个请求,并在前一个请求之前很久就开始获取其源被编译,打包和部署。

通过更改分配给网络中特定任务的操作员数量,您可以调整系统。 例如,如果我们意识到获取源是瓶颈,并且在硬件(网络带宽)仍未得到充分利用的情况下,我们可以通过增加源获取操作员的数量来增加服务器的吞吐量( 图5)。 )。

显然,您的调优选择比派遣经常使用的操作员要走得多。 为了简要说明其他一些可能性,您可以考虑在网络的重复部分之间进行负载平衡,通过同步通信渠道或使用类似于看板的“进行中的 工作”限制方案来实施生产限制。 对于某些问题, 可以考虑数据缓存和推测性计算。

摘要

将脚趾浸入Groovy并发中之后,您可能就可以进行更深入的了解了。 为了快速入门 ,请考虑遵循GPars 快速 通道之一,我绝对建议您阅读用户指南以了解更多详细信息。

如果您乘坐GPar,我会很高兴。 去享受并发,因为coz并发是Groovy!

作者简介: 瓦茨拉夫(Václav) 是一位编程爱好者,他一直在寻找使开发变得更加有效和令人愉快的方法。 他对服务器端Java技术,并发,现代编程语言和DSL特别感兴趣。 他在JetBrains从事MPS项目的工作,是高级开发人员和技术推广人员。 在另一方面,他领导了GPars项目,该项目是一个用于在Groovy和Java中轻松进行并发编程的开源库。

本文最初发表在Java Tech Journal – Groovy Universe中。 在这里阅读更多内容。


翻译自: https://jaxenter.com/tutorial-gpars-making-parallel-systems-groovy-and-java-friendly-104729.html

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值