GPars Logo

要将本指南下载为 PDF - 点击此处


介绍我们的用户指南

如今,主流计算的世界正在快速变化。如果你打开机箱查看计算机内部,你很可能会看到双核处理器,或者如果你使用的是高端计算机,则会看到四核处理器。我们现在都在多处理器系统上运行软件。

为什么人们仍然创建单线程代码?

我们今天和明天编写的代码可能永远不会在单处理器系统上运行:并行硬件已经成为标准。但软件并非如此,至少现在还没有。人们仍然创建单线程代码,尽管它无法利用当前和未来硬件的全部功能。


我们今天编写的代码可能永远不会在单处理器系统上运行!

一些开发人员尝试使用底层的并发原语,如线程和锁或同步块。但是,很明显,应用程序级别使用的共享内存多线程方法带来的麻烦多于解决的问题。底层的并发处理通常很难做对,而且也不太有趣。

随着硬件的这种根本性变化,软件也必须发生巨大变化。更高层的并发和并行概念,如map/reducefork/join演员数据流,为不同类型的解决领域提供了自然的抽象,同时利用了多核硬件。

进入 GPars

认识 GPars,一个面向 JavaGroovy 的开源并发和并行库,它为你提供了一些编写并发和并行代码的高级抽象,例如 Groovy 中的(map/reduce、fork/join、异步闭包、演员、代理、数据流并发和其他概念),这可以使你的 JavaGroovy 代码轻松实现并发和/或并行。

借助 GPars,你的 Java 和/或 Groovy 代码可以轻松地利用目标系统上的所有可用处理器。你可以同时运行多个计算,并行请求网络资源,安全地解决分治问题,执行函数式风格的 map/reduce 或数据并行集合处理,或者围绕演员或数据流模型构建应用程序。

Apache

GPars 项目是在 Apache 2 许可证 下开源的。

如果你正在使用 Groovy 进行商业、开源、教育或任何其他类型的软件项目开发,请下载二进制文件或从 Maven 存储库中进行集成并开始使用。编写高度并发和/或并行 JavaGroovy 代码的大门已经敞开。 尽情享受吧!

鸣谢

如果没有许多个人为 GPars 成为一款可靠的产品而付出的宝贵时间、精力和专业知识,本项目就不会达到现在的水平。首先要提到的就是核心团队成员:

  • Václav Pech

  • Dierk Koenig

  • Alex Tkachman

  • Russel Winder

  • Paul King

  • Jon Kerridge

  • Rafał Sławik

随着时间的推移,许多其他人贡献了自己的想法,提供了有用的反馈,或以某种方式帮助了 GPars。这个群体中有许多人,要列出所有人的名字太多了,但让我们至少列出最活跃的人:

  • Hamlet d’Arcy

  • Hans Dockter

  • Guillaume Laforge

  • Robert Fischer

  • Johannes Link

  • Graeme Rocher

  • Alex Miller

  • Jeff Gortatowsky

  • Jiří Kropáček

  • Jim Northrop


衷心感谢所有人的贡献!

divider

用户指南:入门

一些假设

在我们开始之前,让我们先设定一些假设:

  • 你了解并使用 Groovy 和/或 Java:否则你不会浪费宝贵的时间研究面向 Groovy 和/或 Java 的并发和并行库。

  • 你绝对希望编写采用并发和并行概念的代码。

  • 如果你没有使用 Groovy,你已经做好了为使用 Java 而付出不可避免的冗长代价的准备。

  • 你的代码目标是多核硬件。

  • 你理解在并发和并行代码中,事情可能随时发生,以任何顺序发生,而且更可能的是,不止一件事同时发生。

准备好了吗?

有了这些假设,我们可以开始了。

越来越明显的是,在 JVM 提供的线程/同步/锁级别处理并发和并行,级别太低,无法保证安全和舒适。

许多高级概念,如演员数据流,已经存在了相当长的时间。并行芯片计算机已经投入使用,至少在数据中心,如果不是在桌面上的话,很久以前就已经投入使用了,远早于多核芯片进入主流硬件市场。

所以现在是将这些高级抽象引入主流软件行业的时候了。

这就是 GParsGroovyJava 语言提供的功能,使其能够使用高级抽象,从而使并发和并行软件的开发更容易、更不容易出错。

GPars 中可用的概念可以分为三类:

  • 代码级辅助工具 - 可以应用于代码库的小部分,如单个算法或数据结构,而不会对整个项目架构造成重大改变的结构

    • 并行集合

    • 异步处理

    • Fork/Join (分治)

  • 架构级概念 - 在设计项目结构时需要考虑的结构

    • 演员

    • 通信顺序进程 (CSP)

    • 数据流

    • 数据并行

  • 共享可变状态保护 - 目前使用共享可变状态的 95% 以上可以通过使用正确的抽象来避免。对于剩下的 5% 的用例,即无法避免共享可变状态的用例,仍然需要良好的抽象。

    • 代理

    • 软件事务内存(尚未在 GPars 中完全实现)

下载和安装

GPars 现在作为 Groovy 的一部分进行分发。所以,如果你安装了 Groovy,那么你应该已经有了 GPars。你的 GPars 的确切版本当然取决于你使用的 Groovy 版本。

如果你还没有 GPars,但你使用 Groovy,那么你可能应该升级你的 Groovy


如果你需要它,你可以 从这里下载 Groovy,并 从这里下载 GPars

如果你没有安装 Groovy,但通过依赖项使用 Groovy,或者可能仅仅是拥有 groovy-all 构件,那么你将需要获取 GPars。此外,如果你想要使用与 Groovy 捆绑的版本不同的 GPars 版本,或者拥有一个旧的、没有 GParsGroovy 版本,你无法升级,那么你将需要获取 GPars。下载 GPars 的方法是:

  • 从存储库中下载构件,并手动添加它和所有传递依赖项。

  • 在 Gradle、Maven 或 Ivy(或 Gant、Ant)构建文件中指定依赖项。

  • 使用 Grapes(尤其适用于 **Groovy** 脚本)。

  • 这里 下载并安装它。

如果您正在构建 **Grails** 或 **Griffon** 应用程序,则可以使用相应的插件为您获取我们的 jar 文件。


GPars 构件

如上所述,**GPars** 现在作为 **Groovy** 的标准分发。但是,如果您必须手动管理此依赖项,则 **GPars** 工件位于主 **Maven** 存储库中,并在 Codehaus 主存储库和快照存储库关闭之前位于其中。

发行版本可以在 **Maven** 主存储库中找到,但目前,当前开发版本(SNAPSHOT)位于 Codehaus 快照存储库中。我们正在将其迁移到其他位置。

要从 **Gradle** 或 Grapes 使用 **GPars**,请使用以下规范

**Gradle** 示例
1
"org.codehaus.gpars:gpars:1.2.0"

在这种情况下,您可能需要手动将我们的快照存储库添加到搜索列表中。使用 **Maven** 依赖项为

**Maven** 声明示例
1
2
3
4
5
<dependency>
    <groupId>org.codehaus.gpars</groupId>
    <artifactId>gpars</artifactId>
    <version>1.2.0</version>
</dependency>

传递依赖项

**GPars** 作为库依赖于高于 2.2.1 的 **Groovy** 版本。此外,必须提供 **Fork/Join** 并发库。这是 **Java 7** 的标准配置。

**GPars 2.0** 将依赖于 **Java 8**,并且只能与 **Groovy 3.0** 及更高版本一起使用。

有关更多详细信息,请访问我们 **GPars** 网站上的 集成 页面。


一个 Hello World 示例

设置完成后,请尝试运行以下 **Groovy** 脚本,以确认您的设置正常运行。

**Groovy** 示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import static groovyx.gpars.actor.Actors.actor

/**
 * A demo showing two cooperating actors. The decryptor decrypts received messages
 * and replies them back.  The console actor sends a message to decrypt, prints out
 * the reply and terminates both actors.  The main thread waits on both actors to
 * finish using the join() method to prevent premature exit, since both actors use
 * the default actor group, which uses a daemon thread pool.
 * @author Dierk Koenig, Vaclav Pech
 */

def decryptor = actor {
    loop {
        react { message ->
            if (message instanceof String) reply message.reverse()
            else stop()
        }
    }
}

def console = actor {
    decryptor.send 'lellarap si yvoorG'
    react {
        println 'Decrypted message: ' + it
        decryptor.send false
    }
}

[decryptor, console]*.join()

您应该在控制台中收到一条消息“解密消息:**Groovy** 是并行的”。

Java API

**GPars** 主要设计用于与 **Groovy** 编程语言一起使用。当然,所有 **Java** 和 **Groovy** 程序只是在 JVM 上运行的字节码,因此 **GPars** 可以与 **Java** 源代码一起使用。

尽管旨在面向 **Groovy**,但 **GPars** 坚实的技术基础以及良好的性能特征使其成为 **Java** 程序的优秀库。实际上,**GPars** 的大部分代码是用 **Java** 编写的,因此使用 **GPars** 的 **Java** 应用程序不会产生性能损失。

有关详细信息,请参阅 **Java API** 部分。

要使用 **Java API** 快速测试 **GPars**,请编译并运行以下 **Java** 代码

另一个 **Java** 示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import groovyx.gpars.MessagingRunnable;
import groovyx.gpars.actor.DynamicDispatchActor;

public class StatelessActorDemo {

    public static void main(String[] args) throws InterruptedException {
        final MyStatelessActor actor = new MyStatelessActor();
        actor.start();
        actor.send("Hello");
        actor.sendAndWait(10);

        actor.sendAndContinue(10.0, new MessagingRunnable<String>() {
            @Override protected void doRun(final String s) {
                System.out.println("Received a reply " + s);
            }
        });
    }
}

class MyStatelessActor extends DynamicDispatchActor {
    public void onMessage(final String msg) {
        System.out.println("Received " + msg);
        replyIfExists("Thank you");
    }

    public void onMessage(final Integer msg) {
        System.out.println("Received a number " + msg);
        replyIfExists("Thank you");
    }

    public void onMessage(final Object msg) {
        System.out.println("Received an object " + msg);
        replyIfExists("Thank you");
    }
}
可能需要工件

请记住,您几乎肯定需要将 **Groovy** 工件添加到构建中,以及 **GPars** 工件。**GPars** 在 **Java** 应用程序中可能以 **Java** 速度运行,但它仍然对 **Groovy** 有一些编译依赖项。


代码规范

我们在代码示例中遵循某些约定。了解这些约定可以帮助您更好地阅读和理解 **GPars** 代码示例。

  • 左移 运算符 '<<' 已在 **actor**、**agent** 和 **数据流** 表达式(变量和流)上重载,表示向其发送消息或赋值

使用左移 运算符
myActor << 'message'

myAgent << {account -> account.add('5 USD')}

myDataflowVariable << 120332
  • 在 **actor** 和 **agent** 上,默认的 call() 方法也已重载,表示发送。因此,向 actor 或 agent 发送消息看起来像是一个常规的函数调用。

myActor "message"

myAgent {house -> house.repair()}
  • **GPars** 中的右移 运算符 '>>' 具有绑定时 的含义。因此

 myDataflowVariable >> {value -> doSomethingWith(value)}

将安排闭包仅在myDataflowVariable 绑定到值后运行,并将该值作为参数。

用法

在示例中,我们倾向于静态导入常用的工厂方法

  • GParsPool.withPool()

  • GParsPool.withExistingPool()

  • GParsExecutorsPool.withPool()

  • GParsExecutorsPool.withExistingPool()

  • Actors.actor()

  • Actors.reactor()

  • Actors.fairReactor()

  • Actors.messageHandler()

  • Actors.fairMessageHandler()

  • Agent.agent()

  • Agent.fairAgent()

  • Dataflow.task()

  • Dataflow.operator()

这更多是风格偏好和个人品味问题,但我们认为静态导入使代码更简洁易读。


在 IDE 中进行设置

将 **GPars** jar 文件添加到您的项目中,或在 pom.xml 中定义相应的依赖项,就足以让您在 IDE 中开始使用 **GPars**。

**GPars** DSL 识别

**IntelliJ IDEA** 在免费的社区版 和商业版的终极版 中都将识别 **GPars** 领域特定语言,完成诸如eachParallel()reduce()callAsync() 之类的函数,并对其进行验证。**GPars** 使用 **Groovy** DSL 机制,该机制会在将 **GPars** jar 文件添加到项目中后立即向 IntelliJ IDEA 教授 DSL。

概念的适用性

**GPars** 提供了许多概念可供选择。我们不断构建和更新文档,以帮助用户为他们手头的任务选择合适的抽象级别。有关详细信息,请参阅 概念比较

为了简要总结这些建议,这里有一些基本准则

  • 您正在查看一个集合,需要使用许多漂亮的 **Groovy** 集合方法(如each()collect()find() 等)对其进行**迭代**或处理。假设处理集合中的每个元素与其他项无关,那么使用 **GPars** **并行集合** 可能合适。

  • 如果您有**长时间运行的计算**,该计算可以安全地在后台运行,请使用 **GPars** 中的**异步调用支持**。由于 **GPars** 异步函数可以组合,因此您可以快速并行化这些复杂的函数计算,而无需显式标记独立计算。

  • 假设您需要**并行化**一种算法。您可以识别一组**任务**及其相互依赖关系。这些任务通常不需要共享数据,而是某些任务可能需要等待其他任务完成才能开始。现在您已准备好明确地用代码表达这些依赖关系。使用 **GPars** **数据流任务**,您可以创建内部顺序任务,每个任务都可以与其他任务并发运行。**数据流** 变量和通道为任务提供了声明其依赖关系并安全地交换数据的功能。

  • 也许您无法避免在逻辑中使用**共享可变状态**。多个线程将访问共享数据(其中一些线程)并对其进行修改。传统的锁定和同步方法感觉过于危险或不熟悉?那么,请使用 **agent** 来包装您的数据,并将所有访问序列化到其中。

  • 您正在构建一个具有高并发需求的系统。调整此处的数据结构或那里的任务不足以解决问题。您需要从头开始构建架构,并牢记并发性。**消息传递** 可能是可行的选择。您的选择可能包括

    • **Groovy CSP** 为您提供高度确定性和可组合的并发进程模型。模型围绕**计算** 或**进程** 的概念进行组织,这些计算或进程并发运行,并通过同步通道进行通信。

    • 如果您正在尝试解决复杂的数据处理问题,请考虑使用 **GPars** **数据流运算符** 来构建数据流网络。该概念围绕使用异步通道连接到管道的事件驱动转换进行组织。

    • 如果您需要遵循面向对象范式构建通用、高度并发和可扩展的架构,那么**Actor** 和**Active Object** 将大放异彩。

现在,您可能对当前项目中要使用哪些概念有了更好的了解。请查看我们**用户指南** 中有关它们的更多详细信息。


新增内容

下一个 **GPars 1.3.0** 版本在先前版本的基础上引入了多项增强功能和改进,主要是在数据流领域。

查看 JIRA 发行说明。


项目变更

重大变更

有关错误修复和改进的列表,请参阅 重大变更列表

异步函数

待定

并行集合

待定

Fork / Join

待定

演员

  • 远程 actor

  • 从活动对象传播异常

数据流

  • 远程数据流变量和通道

  • 接受可变数量参数的数据流运算符

  • Select 变得与 @CompileStatic 兼容

Agent

  • 远程 agent

STM

待定

其他

  • 将 JDK 依赖项提升到版本 1.7

  • 将 **Groovy** 依赖项提升到版本 2.2

  • 用 JDK 1.7 中的实现替换了 **jsr-177y fork-join** 池实现

  • 删除了对 **jsr-166y** 的依赖


Java API - 从 Java 使用 GPars

使用 **GPars** 会让人上瘾,我保证。一旦上瘾,您就无法离开它进行编码。如果世界迫使您用 **Java** 编写代码,您仍然可以从许多 **GPars** 功能中受益。

**Java** API 特性

**GPars** 的某些部分在 **Java** 中无关紧要,最好直接使用底层的 **Java** 库

  • 并行集合 - 直到 **GPars 1.3.0** 可用为止,直接使用jsr-166y 库的**并行数组**

  • Fork/Join - 直到 **GPars 1.3.0** 可用为止,直接使用jsr-166y 库的**Fork/Join** 支持

  • 异步函数 - 直接使用 **Java** 执行器服务

**GPars** 的其他部分可以从 **Java** 中使用,就像从 **Groovy** 中使用一样,尽管大多数人会错过 **Groovy** DSL 功能。

**Java** API 中的 **GPars** 闭包

为了克服 **Java** 中缺少闭包作为语言元素的问题,并避免强迫用户通过 **Java** API 直接使用 **Groovy** 闭包,提供了一些方便的包装类来帮助您定义回调、**actor** 主体或**数据流** 任务。

  • groovyx.gpars.MessagingRunnable - 用于单参数回调或 **actor** 主体

  • groovyx.gpars.ReactorMessagingRunnable - 用于 **ReactiveActor** 主体

  • groovyx.gpars.DataflowMessagingRunnable - 用于**数据流** 运算符的主体

这些类可以在 **GPars API** 预期 **Groovy** 闭包的地方使用。


演员

DynamicDispatchActor 以及ReactiveActor 类可以使用与在 **Groovy** 中一样的方式使用

**DynamicDispatchActor** 示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
 import groovyx.gpars.MessagingRunnable;
 import groovyx.gpars.actor.DynamicDispatchActor;

 public class StatelessActorDemo {
     public static void main(String[] args) throws InterruptedException {
         final MyStatelessActor actor = new MyStatelessActor();
         actor.start();
         actor.send("Hello");
         actor.sendAndWait(10);
         actor.sendAndContinue(10.0, new MessagingRunnable<String>() {
             @Override protected void doRun(final String s) {
                 System.out.println("Received a reply " + s);
             }
         });
     }
 }

 class MyStatelessActor extends DynamicDispatchActor {
     public void onMessage(final String msg) {
         System.out.println("Received " + msg);
         replyIfExists("Thank you");
     }

     public void onMessage(final Integer msg) {
         System.out.println("Received a number " + msg);
         replyIfExists("Thank you");
     }

     public void onMessage(final Object msg) {
         System.out.println("Received an object " + msg);
         replyIfExists("Thank you");
     }
 }

**Groovy** 和 **Java** 之间在使用 **GPars** 时存在一些差异,但请注意,回调实例化了MessagingRunnable 类,而不是 **Groovy** 闭包。

**MessagingRunnable** 示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import groovy.lang.Closure;
import groovyx.gpars.ReactorMessagingRunnable;
import groovyx.gpars.actor.Actor;
import groovyx.gpars.actor.ReactiveActor;

public class ReactorDemo {
    public static void main(final String[] args) throws InterruptedException {

        final Closure handler = new ReactorMessagingRunnable<Integer, Integer>() {
            @Override protected Integer doRun(final Integer integer) {
                return integer * 2;
            }
        };
        final Actor actor = new ReactiveActor(handler);
        actor.start();

        System.out.println("Result: " +  actor.sendAndWait(1));
        System.out.println("Result: " +  actor.sendAndWait(2));
        System.out.println("Result: " +  actor.sendAndWait(3));
    }
}

便捷工厂方法

显然,所有用于快速构建 actor 的必要工厂方法都在您预期的地方。

工厂示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import groovy.lang.Closure;
import groovyx.gpars.ReactorMessagingRunnable;
import groovyx.gpars.actor.Actor;
import groovyx.gpars.actor.Actors;

public class ReactorDemo {
    public static void main(final String[] args) throws InterruptedException {
        final Closure handler = new ReactorMessagingRunnable<Integer, Integer>() {
            @Override protected Integer doRun(final Integer integer) {
                return integer * 2;
            }
        };
        final Actor actor = Actors.reactor(handler);

        System.out.println("Result: " +  actor.sendAndWait(1));
        System.out.println("Result: " +  actor.sendAndWait(2));
        System.out.println("Result: " +  actor.sendAndWait(3));
    }
}

代理

Agent 示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
 import groovyx.gpars.MessagingRunnable;
 import groovyx.gpars.agent.Agent;

 public class AgentDemo {

     public static void main(final String[] args) throws InterruptedException {

         final Agent counter = new Agent<Integer>(0);
         counter.send(10);
         System.out.println("Current value: " + counter.getVal());
         counter.send(new MessagingRunnable<Integer>() {
             @Override protected void doRun(final Integer integer) {
                 counter.updateValue(integer + 1);
             }
         });

         System.out.println("Current value: " + counter.getVal());
     }
 }

数据流并发

DataflowVariablesDataflowQueues 都可以在 **Java** 中使用,没有任何问题。只需避免使用方便的重载运算符,直接使用函数,例如bindwhenBoundgetVal 等。

您也可以继续使用**数据流** 任务,将它们传递给RunnableCallable 的实例,就像 groovy 闭包一样。

数据流示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import groovyx.gpars.MessagingRunnable;
import groovyx.gpars.dataflow.DataflowVariable;
import groovyx.gpars.group.DefaultPGroup;

import java.util.concurrent.Callable;

public class DataflowTaskDemo {

    public static void main(final String[] args) throws InterruptedException {
        final DefaultPGroup group = new DefaultPGroup(10);

        final DataflowVariable a = new DataflowVariable();

        group.task(new Runnable() {
            public void run() {
                a.bind(10);
            }
        });

        final Promise result = group.task(new Callable() {
            public Object call() throws Exception {
                return (Integer)a.getVal() + 10;
            }
        });

        result.whenBound(new MessagingRunnable<Integer>() {
            @Override protected void doRun(final Integer integer) {
                System.out.println("arguments = " + integer);
            }
        });

        System.out.println("result = " + result.getVal());
    }
}

数据流运算符

以下示例应该说明 **Groovy** 和 **Java** API 在数据流运算符方面的主要区别。

想法
  • 在接受通道列表以创建运算符或选择器时,使用方便的工厂方法

  • 使用DataflowMessagingRunnable 来指定运算符主体

  • 在主体内部调用getOwningProcessor()以获取操作符,例如绑定输出值。

更多数据流示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import groovyx.gpars.DataflowMessagingRunnable;
import groovyx.gpars.dataflow.Dataflow;
import groovyx.gpars.dataflow.DataflowQueue;
import groovyx.gpars.dataflow.operator.DataflowProcessor;

import java.util.Arrays;
import java.util.List;

public class DataflowOperatorDemo {

    public static void main(final String[] args) throws InterruptedException {
        final DataflowQueue stream1 = new DataflowQueue();
        final DataflowQueue stream2 = new DataflowQueue();
        final DataflowQueue stream3 = new DataflowQueue();
        final DataflowQueue stream4 = new DataflowQueue();

        final DataflowProcessor op1 = Dataflow.selector(Arrays.asList(stream1), Arrays.asList(stream2), new DataflowMessagingRunnable(1) {
            @Override protected void doRun(final Object... objects) {
                getOwningProcessor().bindOutput(2*(Integer)objects[0]);
            }
        });

        final List secondOperatorInput = Arrays.asList(stream2, stream3);

        final DataflowProcessor op2 = Dataflow.operator(secondOperatorInput, Arrays.asList(stream4), new DataflowMessagingRunnable(2) {
            @Override protected void doRun(final Object... objects) {
                getOwningProcessor().bindOutput((Integer) objects[0] + (Integer) objects[1]);
            }
        });

        stream1.bind(1);
        stream1.bind(2);
        stream1.bind(3);
        stream3.bind(100);
        stream3.bind(100);
        stream3.bind(100);
        System.out.println("Result: " + stream4.getVal());
        System.out.println("Result: " + stream4.getVal());
        System.out.println("Result: " + stream4.getVal());
        op1.stop();
        op2.stop();
    }
}

性能

一般来说,无论您是从Groovy还是Java中使用GPars,其开销都是相同的,而且通常很低。例如,GPars actors可以与其他JVM actor选项(如Scala actors)竞争。

由于Groovy代码通常比Java代码运行得慢一些,这是由于动态方法调用造成的,因此您可以考虑用Java编写代码以提高性能。

通常,任务或actor主体内的数字运算或频繁的细粒度方法调用可以通过重写为Java来提高性能。

先决条件

所有GPars集成规则同样适用于Java项目和Groovy项目。您只需要在项目中包含Groovy发行版jar文件,就可以开始使用了。

您可能还想查看我们的示例Java-Maven项目,了解如何将GPars集成到基于Maven的纯Java应用程序中 - Java示例Maven项目


divider

用户指南:数据并行

专注于数据而不是过程有助于我们创建健壮的并发程序。作为程序员,您定义数据以及应该应用于它的函数,然后让底层机制处理数据。通常,会创建一组并发任务并提交给线程池进行处理。

GPars中,GParsPoolGParsExecutorsPool类可以让您访问低级数据并行技术。GParsPool类依赖于JDK 7中引入的Fork/Join实现,并提供出色的功能和性能。GParsExecutorsPool是为那些仍然需要使用旧版Java执行器的人提供的。

GPars低级数据并行涵盖了三个基本领域

  • 并发处理集合

  • 异步运行函数(闭包)

  • 执行Fork/Join(分治)算法


此处描述的API基于使用GParsJDK7。它可以与更高版本的JDK一起使用,但JDK8引入了Streams框架,可以从Groovy直接使用,并且本质上取代了此处涵盖的GPars功能。正在努力提供此处描述的API,该API基于JDK8 Streams框架,供JDK8及更高版本使用,以提供一个简单的升级路径。

并行集合

处理数据通常涉及操作集合。列表、数组、集合、映射、迭代器、字符串。许多其他数据类型可以被视为项目的集合。处理此类集合的常用模式是按顺序逐个获取元素,并对系列中的每个项目执行操作。

例如,min函数,它应该返回集合中最小的元素。当您对数字集合调用min方法时,会创建一个变量(例如minVal)来存储到目前为止看到的最小值,并将其初始化为给定类型的合理值,例如对于整数和浮点数,这可能是零。然后,将遍历集合的元素,并将每个元素与存储的值进行比较。如果某个值小于minVal中当前存储的值,则minVal将更改为存储新看到的较小值。

处理完所有元素后,集合中的最小值将存储在minVal中。

然而,这个解决方案很简单,但在多核和多处理器硬件上是完全错误的。在双核芯片上运行min函数最多可以利用芯片50%的计算能力。在四核芯片上,它将仅为25%。因此,在后一种情况下,该算法实际上浪费了芯片75%的计算能力。

树状结构被证明更适合并行处理。

我们示例中的min函数不需要逐行遍历所有元素,并将它们的值与minVal变量进行比较。相反,它可以利用我们硬件的多核/多处理器特性。

例如,parallel_min函数可以比较集合中相邻值的对(或特定大小的元组),并将元组中最小的值提升到下一轮比较。在不同的元组中搜索“最小值”可以安全地并行进行,因此同一轮中的元组可以由不同的核心同时处理,而不会出现线程之间的竞争或争用。


认识并行数组

虽然不是JDK7的一部分,但extra166y库带来了一种非常方便的抽象,称为并行数组,而GPars利用这种机制来提供非常Groovy API

什么是extra166y ?

extra166yJava集合的实现,它支持使用JSR-166提供的Fork-Join并发框架进行并行操作。它从未成为JDK的一部分,与jsr166y库不同。实际上,extra166yJDK8开始就被Streams框架取代。因此,为了继续支持JDK7GPars在其内部包含了一个extra166y副本,因此没有外部依赖项。

如前所述,正在努力将GPars API 重写为Streams,供JDK8及更高版本的使用者使用。当然,使用JDK8及更高版本的使用者可以直接从Groovy使用Streams

怎么做呢?

GPars以多种方式利用并行数组实现。GParsPoolGParsExecutorsPool类提供了常见Groovy迭代方法的并行变体,如eachcollectfindAll等。

并行示例
1
 def selfPortraits = images.findAllParallel{it.contains me}.collectParallel{it.resize()}

它还允许更具功能性的map/reduce样式的集合处理。

Map/Reduce示例
1
 def smallestSelfPortrait = images.parallel.filter{it.contains me}.map{it.resize()}.min{it.sizeInMB}

GParsPool

使用GParsPool - 基于JSR-166y的并发集合处理器

用法

GParsPool类提供(来自JSR-166y),一个基于ParallelArray的并发DSL,用于集合和对象。

使用示例

一些并行示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Summarize numbers concurrently.
GParsPool.withPool {
    final AtomicInteger result = new AtomicInteger(0)
    [1, 2, 3, 4, 5].eachParallel{result.addAndGet(it)}

    assert 15 == result
}

// Multiply numbers asynchronously.
GParsPool.withPool {
    final List result = [1, 2, 3, 4, 5].collectParallel{it * 2}

    assert ([2, 4, 6, 8, 10].equals(result))
}

传递的闭包将ForkJoinPool的实例作为参数,然后可以在闭包内部自由使用。

ForkJoinPool示例
1
2
3
4
5
6
// Check whether all elements within a collection meet certain criteria.
GParsPool.withPool(5){ForkJoinPool pool ->
    assert [1, 2, 3, 4, 5].everyParallel{it > 0}

    assert ![1, 2, 3, 4, 5].everyParallel{it > 1}
}

GParsPool.withPool方法接受创建池中的线程数量的可选参数,以及一个未处理异常处理程序。

包含线程数量的异常处理程序示例
1
2
withPool(10){...}
withPool(20, exceptionHandler){...}

池重用

GParsPool.withExistingPool接受一个已经存在的ForkJoinPool实例以重用。DSL仅在相关代码块内有效,并且仅对调用了withPoolwithExistingPool方法的线程有效。withPool方法仅在所有工作线程完成其任务并销毁池后才返回,返回相关代码块的最终结果。withExistingPool方法不会等待池线程完成。

或者,GParsPool类可以静态导入为import static groovyx.gpars.GParsPool,因此我们可以省略GParsPool类名。

池示例
1
2
3
4
withPool {
    assert [1, 2, 3, 4, 5].everyParallel{it > 0}
    assert ![1, 2, 3, 4, 5].everyParallel{it > 1}
}

以下方法目前在Groovy中的所有对象上都受支持

  • eachParallel

  • eachWithIndexParallel

  • collectParallel

  • collectManyParallel

  • findAllParallel

  • findAnyParallel

  • findParallel

  • everyParallel

  • anyParallel

  • grepParallel

  • groupByParallel

  • foldParallel

  • minParallel

  • maxParallel

  • sumParallel

  • splitParallel

  • countParallel

  • foldParallel

元类增强器

作为替代方案,您可以使用ParallelEnhancer类来增强任何类或单个实例的元类,使其具有并行方法。

增强示例
1
2
3
4
5
6
7
8
9
10
import groovyx.gpars.ParallelEnhancer

def list = [1, 2, 3, 4, 5, 6, 7, 8, 9]
ParallelEnhancer.enhanceInstance(list)
println list.collectParallel {it * 2 }

def animals = ['dog', 'ant', 'cat', 'whale']
ParallelEnhancer.enhanceInstance animals
println (animals.anyParallel {it ==~ /ant/} ? 'Found an ant' : 'No ants found')
println (animals.everyParallel {it.contains('a')} ? 'All animals contain a' : 'Some animals can live without an a')

使用ParallelEnhancer类时,您在使用GParsPool DSL时无需限制在withPool块中。增强后的类或实例将保持增强状态,直到它们被垃圾回收。

异常处理

如果在处理任何传递的闭包时抛出异常,第一个异常将从xxxParallel方法中重新抛出,并且算法将在第一时间停止。

异常处理

GParsPool的异常处理机制建立在Fork/Join框架中提供的机制之上。由于Fork/Join算法本质上是分层的,因此一旦算法的任何部分失败,通常继续计算几乎没有好处,因为算法的某些分支永远不会返回结果。

请记住,GParsPool实现不保证在发生第一个未处理异常后的行为,除了停止算法并将检测到的第一个异常重新抛出给调用者之外。毕竟,这种行为与传统的顺序迭代方法所做的一致。

透明并行集合

除了添加新的xxxParallel方法之外,GPars还可以让您更改原始迭代方法的语义。

例如,您可能将一个集合传递给一个库方法,该方法将以顺序方式处理您的集合,例如,使用collect方法。然后,通过更改集合上collect方法的语义,您可以有效地并行化此库顺序代码。

makeConcurrent() 示例
1
2
3
4
5
6
7
8
9
10
11
12
GParsPool.withPool {

    //The selectImportantNames() will process the name collections concurrently
    assert ['ALICE', 'JASON'] == selectImportantNames(['Joe', 'Alice', 'Dave', 'Jason'].makeConcurrent())
}

/**
 * A function implemented using standard sequential collect() and findAll() methods.
 */
def selectImportantNames(names) {
    names.collect {it.toUpperCase()}.findAll{it.size() > 4}
}

makeSequential方法会将集合重置回原始的顺序语义。

顺序示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import static groovyx.gpars.GParsPool.withPool

def list = [1, 2, 3, 4, 5, 6, 7, 8, 9]

println 'Sequential: ' list.each { print it + ',' } println()

withPool {

    println 'Sequential: '
    list.each { print it + ',' }
    println()

    list.makeConcurrent()

    println 'Concurrent: '
    list.each { print it + ',' }
    println()

    list.makeSequential()

    println 'Sequential: '
    list.each { print it + ',' }
    println()
}

println 'Sequential: '
list.each { print it + ',' }
println()

asConcurrent()便捷方法允许我们指定代码块,在这些代码块中,集合保持并发语义。

asConcurrent() 示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import static groovyx.gpars.GParsPool.withPool

def list = [1, 2, 3, 4, 5, 6, 7, 8, 9]

println 'Sequential: '
list.each { print it + ',' }
println()

withPool {

    println 'Sequential: '
    list.each { print it + ',' }
    println()

    list.asConcurrent {
        println 'Concurrent: '
        list.each { print it + ',' }
        println()
    }

    println 'Sequential: '
    list.each { print it + ',' }
    println()
}

println 'Sequential: '
list.each { print it + ',' }
println()

代码示例

透明并行,包括makeConcurrent()makeSequential()asConcurrent()方法,也可以与我们的ParallelEnhancer结合使用。

ParallelEnhancer 示例
1
2
3
4
5
6
7
8
9
10
11
12
/**
 * A function implemented using standard sequential collect() and findAll() methods.
 */
def selectImportantNames(names) {
    names.collect {it.toUpperCase()}.findAll{it.size() > 4}
}

def names = ['Joe', 'Alice', 'Dave', 'Jason']
ParallelEnhancer.enhanceInstance(names)

//The selectImportantNames() will process the name collections concurrently
assert ['ALICE', 'JASON'] == selectImportantNames(names.makeConcurrent())
另一个 ParallelEnhancer 示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import groovyx.gpars.ParallelEnhancer

def list = [1, 2, 3, 4, 5, 6, 7, 8, 9]

println 'Sequential: '
list.each { print it + ',' }
println()

ParallelEnhancer.enhanceInstance(list)

println 'Sequential: '
list.each { print it + ',' }
println()

list.asConcurrent {
    println 'Concurrent: '
    list.each { print it + ',' }
    println()

}
list.makeSequential()

println 'Sequential: '
list.each { print it + ',' }
println()

避免函数中的副作用

我们必须警告您。由于提供给并行方法(如eachParallelcollectParallel())的闭包可能并行运行,因此您必须确保每个闭包都以线程安全的方式编写。闭包不能持有任何内部状态,共享数据,也不得在它们被调用的单个元素的边界之外产生副作用。违反这些规则将为竞争条件和死锁打开大门,这是现代多核程序员最严重的敌人。


不要这样做!

并发访问非线程安全集合
1
2
def thumbnails = []
images.eachParallel {thumbnails << it.thumbnail}  //Concurrently accessing a not-thread-safe collection of thumbnails? Don't do this!

至少,您已经得到警告。

它可能不会按您的预期执行

由于GParsPool使用Fork/Join池(带有工作窃取),因此即使线程可能看起来处于空闲状态,它们也可能不会应用于等待处理的任务。

使用工作窃取算法,用完工作要做的工作线程可以从其他仍在忙碌的线程中窃取任务。

如果您使用GParsExecutorsPool(它不使用Fork/Join),您将获得您天真地期望的线程分配行为。

GParsExecutorsPool

使用GParsExecutorsPool - 基于Java Executors的并发集合处理器 -

GParsExecutorsPool 的用法

GParsPool类启用了一个基于Java Executors的并发DSL,用于集合和对象。

GParsExecutorsPool类可以用作纯JDK的集合并行处理器。与GParsPool类不同,GParsExecutorsPool不需要fork/join线程池,而是利用标准的JDK执行器服务来并行化闭包以迭代地处理集合或对象。

然而,必须指出,GParsPool通常比GParsExecutorsPool的性能好得多。


GParsPool通常比GParsExecutorsPool的性能好得多

使用示例

GParsExecutorsPool 示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//multiply numbers asynchronously
 GParsExecutorsPool.withPool {
     Collection<Future> result = [1, 2, 3, 4, 5].collectParallel{it * 10}

     assert new HashSet([10, 20, 30, 40, 50]) == new HashSet((Collection)result*.get())
 }

 //multiply numbers asynchronously using an asynchronous closure
 GParsExecutorsPool.withPool {
     def closure={it * 10}
     def asyncClosure=closure.async()

     Collection<Future> result = [1, 2, 3, 4, 5].collect(asyncClosure)

     assert new HashSet([10, 20, 30, 40, 50]) == new HashSet((Collection)result*.get())
 }

传入的闭包以 ExecutorService 实例作为参数,可以在闭包内自由使用。

另一个 GParsExecutorsPool 示例
1
2
3
4
//find an element meeting specified criteria
 GParsExecutorsPool.withPool(5) {ExecutorService service ->
     service.submit({performLongCalculation()} as Runnable)
 }

GParsExecutorsPool.withPool() 方法接收一个可选参数,用于声明创建的池中的线程数和线程工厂。

声明所需线程数的示例
1
2
withPool(10) {...}
withPool(20, threadFactory) {...}

GParsExecutorsPool.withExistingPool() 接收一个已存在的 executor service 实例 以供重用。DSL 仅在关联的代码块内有效,并且仅对调用过 withPool()withExistingPool() 方法的线程有效。


您知道 withExistingPool() 方法不会等待 executor service 线程 完成吗?

withPool() 方法仅在所有工作线程完成其任务并且 executor service 被销毁后才返回控制权,并返回关联代码块的最终结果值。


GParsExecutorsPool 类静态导入为 import static groovyx.gpars.GParsExecutorsPool.* 以省略 GParsExecutorsPool 类名。

FindParallel 示例
1
2
3
4
withPool {
     def result = [1, 2, 3, 4, 5].findParallel{Number number -> number > 2}
     assert result in [3, 4, 5]
 }

以下方法当前在支持 Groovy 中迭代的所有对象上受支持

  • eachParallel()

  • eachWithIndexParallel()

  • collectParallel()

  • findAllParallel()

  • findParallel()

  • allParallel()

  • anyParallel()

  • grepParallel()

  • groupByParallel()


元类增强器

或者,您可以使用 GParsExecutorsPoolEnhancer 类来增强任何具有异步方法的类的元类或单个实例。

增强您的代码
1
2
3
4
5
6
7
8
9
10
11
import groovyx.gpars.GParsExecutorsPoolEnhancer

def list = [1, 2, 3, 4, 5, 6, 7, 8, 9]
GParsExecutorsPoolEnhancer.enhanceInstance(list)
println list.collectParallel {it * 2 }

def animals = ['dog', 'ant', 'cat', 'whale']
GParsExecutorsPoolEnhancer.enhanceInstance animals

println (animals.anyParallel {it ==~ /ant/} ? 'Found an ant' : 'No ants found')
println (animals.allParallel {it.contains('a')} ? 'All animals contain a' : 'Some animals can live without an a')

使用 GParsExecutorsPoolEnhancer 类时,您不受限于使用 GParsExecutorsPool DSLwithPool() 块。增强后的类或实例将一直保持增强状态,直到它们被垃圾回收。

异常处理

在处理任何传入的闭包时可能会抛出异常。AsyncException 方法的实例将包装从 xxxParallel 方法重新抛出的任何/所有原始异常。

避免函数中的副作用

我们再次需要警告您使用具有副作用的闭包。请避免影响超出单个当前处理元素范围的对象的逻辑。请避免影响状态的逻辑或闭包。不要这样做!将它们传递给任何 xxxParallel() 方法都是危险的。


记忆化

memoize 函数允许缓存函数的返回值。对记忆化函数的重复调用,如果具有相同的参数值,将从内部透明缓存中检索结果值,而不是调用原始函数中编码的计算。

如果计算明显比从缓存中检索缓存值慢,开发人员可以权衡内存和性能。

查看示例,我们在其中尝试扫描多个网站以查找特定内容

GParsmemoize 功能已捐赠给 Groovy 1.8 版本,如果您在 Groovy 1.8 或更高版本上运行,建议您使用 Groovy 功能。

GPars 中的 Memoize 几乎相同,只是它使用周围的线程池并发地搜索记忆化的缓存。这在某些情况下可能会带来性能优势。

Memoize Me Up, Scotty

GPars memoize 功能已重命名,以避免将来与 Groovy 中的 memoize 功能发生冲突。

GPars 现在使用前导字母 g 调用这些方法,例如 gmemoize()

使用示例

使用 gmemoize() 的 GParsPool 示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
GParsPool.withPool {
    def urls = ['http://www.dzone.com', 'http://www.theserverside.com', 'http://www.infoq.com']

    Closure download = {url ->
        println "Downloading $url"
        url.toURL().text.toUpperCase()
    }

    Closure cachingDownload = download.gmemoize()

    println 'Groovy sites today: ' + urls.findAllParallel {url -> cachingDownload(url).contains('GROOVY')}
    println 'Grails sites today: ' + urls.findAllParallel {url -> cachingDownload(url).contains('GRAILS')}
    println 'Griffon sites today: ' + urls.findAllParallel {url -> cachingDownload(url).contains('GRIFFON')}
    println 'Gradle sites today: ' + urls.findAllParallel {url -> cachingDownload(url).contains('GRADLE')}
    println 'Concurrency sites today: ' + urls.findAllParallel {url -> cachingDownload(url).contains('CONCURRENCY')}
    println 'GPars sites today: ' + urls.findAllParallel {url -> cachingDownload(url).contains('GPARS')}
}

请注意,闭包如何在 GParsPool.withPool() 块内使用 memoize() 函数进行增强。这将返回一个包装原始闭包的新闭包作为缓存条目。

在前面的示例中,我们在代码中的多个地方调用了 cachingDownload 函数,但是每个唯一的 URL 仅下载一次——第一次需要时。然后将这些值缓存起来,并可用于后续调用。此外,这些值也可供所有线程使用,无论哪个线程最初对该特定 URL 发出下载请求,并需要处理实际的计算/下载。

因此,总结一下,memoize 调用通过使用过去返回值的缓存来保护函数。

但是,memoize 可以做得更多!在某些算法中,添加一些内存可能会对计算的计算复杂度产生重大影响。让我们看一个 Fibonacci 数的经典示例。

斐波那契数列示例

完全函数式递归实现遵循 Fibonacci 数的定义,其复杂度呈指数级增长

Fibonacci 示例
1
Closure fib = {n -> n > 1 ? call(n - 1) + call(n - 2) : n}

尝试使用大约 30 的数字调用 fib 函数,您会发现它有多慢。

现在,稍微调整一下,添加一个 memoize 缓存,该算法神奇地变成了线性复杂度算法

Fibonacci 示例的改进版本
1
2
Closure fib
fib = {n -> n > 1 ? fib(n - 1) + fib(n - 2) : n}.gmemoize()

我们添加的额外内存现在已经切断了除一个递归分支之外的所有分支。所有后续对相同 fib 函数的调用也将受益于缓存的值。

请查看下面如何使用 memoizeAtMost 变体来减少我们示例中的内存消耗,同时保持算法的线性复杂度。


可用变体

记忆化

基本变体在记忆化函数的整个生命周期内将值保留在内部缓存中。它提供了所有变体中最好的性能特性。

memoizeAtMost

允许我们设置缓存项数量的硬性限制。一旦达到限制,所有随后添加的值将使用 LRU最近最少使用)策略从缓存中消除最旧的值。

因此,对于我们的 Fibonacci 数示例,我们可以安全地将缓存大小减少到两个项目

缓存的 Fibonacci 示例
1
2
Closure fib
fib = {n -> n > 1 ? fib(n - 1) + fib(n - 2) : n}.memoizeAtMost(2)

对缓存大小设置上限有两个目的

  • 将缓存的内存占用保持在定义的范围内

  • 保留函数所需的性能特性。与直接计算结果所需的时间相比,过大的缓存会增加检索缓存值所需的时间。

memoizeAtLeast

允许内部缓存无限增长,直到 JVM 的垃圾收集器决定介入并从内存中驱逐 SoftReferences 条目(由我们的实现使用)。

memoizeAtLeast() 方法的单个参数指示应保护不受 gc 驱逐的最小缓存项数。缓存永远不会缩减到指定的条目数以下。缓存确保它只使用 LRU(最近最少使用)策略保护最近使用的项目免遭驱逐。

memoizeBetween

结合 memoizeAtLeastmemoizeAtMost 方法,允许缓存根据可用内存和 gc 活动在两个参数值之间的范围内增长和缩减。

缓存大小永远不会超过上限,以保持缓存所需的性能特性。


Map-Reduce

并行集合映射/规约 DSL 为 GPars 提供了更函数式的风格。一般来说,映射/规约 DSL 可用于与 xxxParallel() 方法族相同的目的,并且具有非常相似的语义。另一方面,如果需要将多个方法链接在一起以在多个步骤中处理单个集合,映射/规约 的执行速度可能快得多

映射/规约 示例
1
2
3
4
5
6
    println 'Number of occurrences of the word GROOVY today: ' + urls.parallel
            .map {it.toURL().text.toUpperCase()}
            .filter {it.contains('GROOVY')}
            .map{it.split()}
            .map{it.findAll{word -> word.contains 'GROOVY'}.size()}
            .sum()

xxxParallel() 方法必须遵循与其非并行对等方法相同的约定。因此,collectParallel() 方法必须返回一个合法的项目集合,您可以将其视为 Groovy 集合。

在内部,并行 collect 方法 建立一个称为 并行数组 的高效并行结构。然后它并发地执行所需的操作。在返回之前,它会销毁 并行数组,因为它正在构建一个结果集合以返回给您。例如,对结果集合进行潜在的 findAllParallel() 调用将在幕后重复构建和销毁 并行数组 实例的整个过程。

使用 映射/规约,您只需将集合转换为 并行数组 并转换回来一次。映射/规约 方法族不会返回 Groovy 集合,但可以自由地直接传递内部 并行数组

调用集合的 parallel 属性将为该集合构建一个 并行数组,然后返回一个围绕 并行数组 实例的薄包装器。然后,您可以将这些方法中的任何一个链接在一起以获取答案

  • map()

  • reduce()

  • filter()

  • size()

  • sum()

  • min()

  • max()

  • sort()

  • groupBy()

  • combine()

返回一个普通的 Groovy 集合实例始终只是检索 collection 属性的问题。

映射/规约 示例
1
def myNumbers = (1..1000).parallel.filter{it % 2 == 0}.map{Math.sqrt it}.collection

避免函数中的副作用

我们再次需要警告您。为了避免出现意外情况,请确保您传递给 映射/规约 函数的任何闭包都是无状态的,并且没有副作用。


为了避免出现意外情况,请确保您的闭包是无状态的

可用性

此功能仅在使用基于 Fork/JoinGParsPool 时可用,在 GParsExecutorsPool 方法中不可用。

经典示例

一个经典的示例,受 thevery 启发,统计字符串中单词出现的次数

一个有说服力的示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import static groovyx.gpars.GParsPool.withPool

def words = "This is just plain text to count words in"
print count(words)

def count(arg) {

  withPool {

    return arg.parallel
      .map{[it, 1]}
      .groupBy{it[0]}.getParallel()
      .map {it.value=it.value.size();it}
      .sort{-it.value}.collection
  }

}

同一个示例可以使用更通用的 combine 操作来实现

Combine 示例
1
2
3
4
5
6
7
8
9
10
11
12
13
def words = "This is just plain text to count words in"
print count(words)

def count(arg) {

  withPool {
    return arg.parallel
      .map{[it, 1]}
      .combine(0) {sum, value -> sum + value}.getParallel()
      .sort{-it.value}.collection
  }

}

合并

combine 操作期望一个输入元组列表(双元素列表),通常被认为是键值对(例如 [ [key1, value1], [key2, value2], [key1, value3], [key3, value4] …​ ])。这些键可能重复。

调用时,combine 方法使用提供的累加器函数合并相同键的值。这会生成一个包含原始(唯一)键及其(现在)累加值的映射。

例如, 将合并为 [a : b+e, c : d+f]。需要提供一些逻辑(例如,值的 “+” 操作)作为累加闭包逻辑。

累加器函数 参数需要指定一个函数,在合并(累加)属于同一键的值时使用该函数。还需要提供一个 初始累加器值

由于 combine 方法并行处理项目,因此 初始累加器值 将被多次重用。因此,提供的 value 必须允许重用。

它可以是一个 可克隆的(或 不可变的)值,也可以是一个每次请求时返回一个新的初始累加器的 闭包。累加器函数和可重用初始值的良好组合包括

结合累加器函数和可重用初始值的一些示例
1
2
3
4
5
accumulator = {List acc, value -> acc << value} initialValue = []
accumulator = {List acc, value -> acc << value} initialValue = {-> []}
accumulator = {int sum, int value -> acc + value} initialValue = 0
accumulator = {int sum, int value -> sum + value} initialValue = {-> 0}
accumulator = {ShoppingCart cart, Item value -> cart.addItem(value)} initialValue = {-> new ShoppingCart()}

返回类型为映射。

例如,[['he', 1], ['she', 2], ['he', 2], ['me', 1], ['she', 5], ['he', 1]] 的初始值为零,将合并为 ['he' : 4, 'she' : 7, 'me' : 1]

比较逻辑

键将使用它们的 equalshashCode 方法进行相互比较。请考虑使用 @Canonical@EqualsAndHashCode 注释来注释您用作键的对象。

Groovy 中的所有哈希映射一样,请确保您使用的是 String,而不是 GString 作为键!

对于更复杂的情况,当您 combine() 复杂对象时,一个好的策略是在这里有一个完整的类用作常见用例的键,并对不常见的用例应用不同的键。

一个复杂的示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
import groovy.transform.ToString
import groovy.transform.TupleConstructor

import static groovyx.gpars.GParsPool.withPool

// declare a complete class to use in combination processing
@TupleConstructor @ToString
class PricedCar implements Cloneable {        // either Clonable or Immutable
    String model
    String color
    Double price

        // declare a way to resolve comparison logic
    boolean equals(final o) {
        if (this.is(o)) return true
        if (getClass() != o.class) return false

        final PricedCar pricedCar = (PricedCar) o

        if (color != pricedCar.color) return false
        if (model != pricedCar.model) return false

        return true
    }

    int hashCode() {
        int result
        result = (model != null ? model.hashCode() : 0)
        result = 31 * result + (color != null ? color.hashCode() : 0)
        return result
    }

    @Override
    protected Object clone() {
        return super.clone()
    }
}

// some data
def cars = [new PricedCar('F550', 'blue', 2342.223),
        new PricedCar('F550', 'red', 234.234),
        new PricedCar('Da', 'white', 2222.2),
        new PricedCar('Da', 'white', 1111.1)]


withPool {
    //Combine by model
    def result =
        cars.parallel.map {
            [it.model, it]
        }.combine(new PricedCar('', 'N/A', 0.0)) {sum, value ->
            sum.model = value.model
            sum.price += value.price
            sum
        }.values()

    println result


    //Combine by model and color (using the PricedCar's equals and hashCode))
    result =
        cars.parallel.map {
            [it, it]
        }.combine(new PricedCar('', 'N/A', 0.0)) {sum, value ->
            sum.model = value.model
            sum.color = value.color
            sum.price += value.price
            sum
        }.values()

    println result
}

并行数组

或者,可以直接使用 JSR-166y - Java 并发 中定义的基于树的高效数据结构。任何集合或对象的 parallelArray 属性将返回一个 ParallelArray 实例,该实例保存原始集合的元素。然后,这些可以通过 jsr166y API 进行操作。

有关 API 详细信息,请参阅 jsr166y 文档。

并行数组示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import groovyx.gpars.extra166y.Ops

groovyx.gpars.GParsPool.withPool {

    assert 15 == [1, 2, 3, 4, 5].parallelArray.reduce({a, b -> a + b} as Ops.Reducer, 0)                                        //summarize

    assert 55 == [1, 2, 3, 4, 5].parallelArray.withMapping({it ** 2} as Ops.Op).reduce({a, b -> a + b} as Ops.Reducer, 0)       //summarize squares

    assert 20 == [1, 2, 3, 4, 5].parallelArray.withFilter({it % 2 == 0} as Ops.Predicate)                                       //summarize squares of even numbers
            .withMapping({it ** 2} as Ops.Op)
            .reduce({a, b -> a + b} as Ops.Reducer, 0)

    assert 'aa:bb:cc:dd:ee' == 'abcde'.parallelArray                                                                            //concatenate duplicated characters with separator
            .withMapping({it * 2} as Ops.Op)
            .reduce({a, b -> "$a:$b"} as Ops.Reducer, "")

异步调用

大多数系统中都会出现长时间运行的后台任务。

通常,执行的主线程希望初始化一些计算,启动下载,进行搜索等,即使结果可能不需要立即获得。

GPars 为开发人员提供了工具,可以将异步活动安排到后台处理,并在需要时收集结果。

GParsPoolGParsExecutorsPool 异步处理工具的使用

GParsPoolGParsExecutorsPool 方法都提供几乎相同的功能,但利用不同的底层机制。

闭包增强

以下方法被添加到 GPars(Executors)Pool.withPool() 块中的闭包内

  • async() - 创建提供闭包的异步变体,当调用时,返回一个 future 对象,用于表示潜在的返回值

  • callAsync() - 在单独的线程中调用一个闭包,同时提供给定的参数,并返回一个 future 对象,用于表示潜在的返回值

一个 async() 示例
1
2
3
4
5
6
7
8
9
GParsPool.withPool() {
    Closure longLastingCalculation = {calculate()}
    Closure fastCalculation = longLastingCalculation.async()  //create a new closure, which starts the original closure on a thread pool

    Future result=fastCalculation()                           //returns almost immediately

    //do stuff while calculation performs ...
    println result.get()
}
一个 callAsync() 示例
1
2
3
4
5
6
7
8
GParsPool.withPool() {
    /**
     * The callAsync() method is an asynchronous variant of the default call() method to invoke a closure.
     * It will return a Future for the result value.
     */
    assert 6 == {it * 2}.call(3)
    assert 6 == {it * 2}.callAsync(3).get()
}

超时

callTimeoutAsync() 方法,接收一个长整型值或一个 Duration 实例,提供了计时器机制。

一个计时示例
1
2
3
4
5
6
{->
    while(true) {
        Thread.sleep 1000  //Simulate a bit of interesting calculation
        if (Thread.currentThread().isInterrupted()) break;  //We've been cancelled
    }
}.callTimeoutAsync(2000)

为了允许取消,我们异步运行的代码必须持续检查其自身线程的 interrupted 标志,并在标志被设置为 true 时停止计算。

执行器服务增强

ExecutorServiceForkJoinPool 类使用 '<<'(左移)运算符增强,将任务提交到池中,并返回一个 Future 用于表示结果。

使用 [red]'<<' 的便捷示例
1
2
3
GParsExecutorsPool.withPool {ExecutorService executorService ->
    executorService << {println 'Inside parallel task'}
}

并行运行函数(闭包)

GParsPoolGParsExecutorsPool 类还提供了方便的方法 executeAsync()executeAsyncAndWait() ,用于轻松地异步运行多个闭包。

示例

一个示例
1
2
3
4
GParsPool.withPool {
    assert [10, 20] == GParsPool.executeAsyncAndWait({calculateA()}, {calculateB()}         //waits for results
    assert [10, 20] == GParsPool.executeAsync({calculateA()}, {calculateB()})*.get()  //returns Futures instead and doesn't wait for results to be calculated
}

可组合的异步函数

函数需要被组合。实际上,组合无副作用的函数非常容易。比组合对象(例如)更容易更可靠。

在给定相同输入的情况下,函数始终返回相同的结果,它们不会意外地更改行为,也不会在多个线程同时调用它们时崩溃。

Groovy 中的函数

我们可以将 Groovy 闭包视为函数。它们接受参数,执行计算并返回一个值。只要你不让你的闭包访问其范围之外的任何东西,你的闭包就表现良好,就像纯函数一样。你可以为了更高的目标而组合的函数。

一个更高的目标示例
1
def sum = (0..100000).inject(0, {a, b -> a + b})

对于这个示例,通过将一个添加两个数字 {a,b} 的函数与 inject 函数(它遍历整个集合)组合,你可以快速地总结所有项。然后,将 添加 函数替换为 比较 函数,会立即得到一个组合函数来计算最大值。

查找最大值
1
def max = myNumbers.inject(0, {a, b -> a>b?a:b})

你看,函数式编程流行是有原因的。

我们现在是并发了吗?

这一切都运行良好,直到你意识到你并没有使用昂贵硬件的全部能力。这些函数只是简单的顺序执行!没有使用并行处理!除了一个处理器核心之外,所有其他核心都处于空闲状态,完全浪费了!

使用异步函数的通用方法

那些注意观察的人可能会决定使用前面描述的 并行集合 技术,他们当然是对的。

对于我们在这里描述的场景,我们处理一个集合,使用那些 并行 方法将是最好的选择。但是,我们现在正在寻找一种通用方法来创建和组合异步函数。这将帮助我们,不仅用于集合处理,而且大多数情况下在其他更通用的情况下,比如下面的情况。


除了一个处理器核心之外,所有其他核心都处于空闲状态!它们处于空闲状态!完全浪费了!

为了使事情更清楚,这里有一个组合四个函数的示例,这些函数应该检查特定网页是否匹配本地文件的內容。我们需要下载页面,加载文件,计算两者的哈希值,最后比较结果数字。

一个示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Closure download = {String url ->
    url.toURL().text
}

Closure loadFile = {String fileName ->
    ...  //load the file here
}

Closure hash = {s -> s.hashCode()}

Closure compare = {int first, int second ->
    first == second
}

def result = compare(hash(download('http://www.gpars.org')), hash(loadFile('/coolStuff/gpars/website/index.html')))
println "The result of comparison: " + result

我们需要下载页面,加载文件,计算两者的哈希值,最后比较结果数字。每个函数负责一项特定的工作。一个函数下载內容,第二个函数加载文件,第三个函数计算哈希值,最后第四个函数进行比较。

组合函数就像嵌套它们的调用一样简单。


使一切异步化

我们代码的缺点是我们没有利用 download()loadFile() 函数的独立性。我们也没有允许两个哈希值同时运行。它们可以很好地并行运行,但是我们组合函数的方法限制了并行性。

显然,并非所有函数都 **可以** 并行运行。一些函数依赖于其他函数的结果。在另一个函数完成之前,它们不能开始。我们需要阻塞它们,直到它们的參數可用。hash() 函数需要一个字符串来处理。compare() 函数需要两个数字来比较。

因此,我们只能在一定程度上进行并行处理,而其他函数则被阻塞。这似乎是一项具有挑战性的任务。

函数式世界一片光明

幸运的是,函数之间的依赖关系已经在代码中隐式表达。没有必要重复该依赖关系信息。如果一个函数接受参数,而参数需要由另一个函数先计算,那么我们在这里隐式地有一个依赖关系。

在我们的示例中,hash() 函数依赖于 loadFile() 以及 download() 函数。我们之前示例中的 inject 函数依赖于依次对集合中所有元素调用的 addition 函数的结果。

实际上,我们的任务非常简单!

无论乍看之下有多么困难,实际上,我们的任务非常简单。我们只需要教我们的函数返回其未来结果的 承诺 。我们还需要教其他函数接受这些 承诺 作为参数,以便它们在开始工作之前等待实际的值。

如果我们说服函数释放它们持有的线程,并在等待值的同时,我们直接到达可以发生神奇的地方。

秉承 GPars 的优良传统,我们使你很容易说服任何函数相信其他函数的 承诺 。对闭包调用 asyncFun() 函数,你就可以异步执行了!

承诺,承诺
1
2
3
4
5
6
withPool {
    def maxPromise = numbers.inject(0, {a, b -> a>b?a:b}.asyncFun())

    println "Look Ma, I can talk to the user while the math is being done for me!"
    println maxPromise.get()
}

inject 函数并不真正关心 addition 函数返回什么对象,也许它有点惊讶每次调用 addition 函数都返回得如此快,但并没有抱怨太多,继续迭代,最终返回我们期望的总结果。

现在是你应该信守承诺并做你想让别人做的事情的时候了。不要对结果皱眉,只接受你得到了一个 承诺 。一个在计算完成后尽快交付答案的 承诺 。你笔记本电脑的额外热量表明,该计算利用了函数中的自然并行性,并尽最大努力尽快将结果交付给你。

承诺就是承诺

承诺 是一个老式的 DataflowVariable ,因此你可以查询其状态,注册一些通知钩子,甚至将它作为 Dataflow 算法的输入!

一个有希望的示例
1
2
3
4
5
6
7
withPool {
    def sumPromise = (0..100000).inject(0, {a, b -> a + b}.asyncFun())

    println "Are we done yet? " + sumPromise.bound

    sumPromise.whenBound {sum -> println sum}
}
你需要超时吗?

get() 方法还有一个带有超时参数的变体,如果你想避免无限期等待的风险。

事情会出错吗?

当然。但是你会从 promiseget() 方法中抛出异常。

一个异常示例
1
2
3
4
5
6
try {
    sumPromise.get()

} catch (MyCalculationException e) {
    println "Guess, things are not ideal today."
}

这一切都很好,但是哪些函数可以真正组合起来?

你的雄心壮志没有限制。无论你需要组合哪些顺序函数,你都应该能够组合它们的异步变体。

回顾我们最初比较文件內容和网页內容的示例。我们只需通过对所有函数调用 asyncFun() 方法来使它们异步化,我们就可以出发了。

使用 asyncFun() 示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
    Closure download = {String url ->
        url.toURL().text
    }.asyncFun()

    Closure loadFile = {String fileName ->
        ...  //load the file here
    }.asyncFun()

    Closure hash = {s -> s.hashCode()}.asyncFun()

    Closure compare = {int first, int second ->
        first == second
    }.asyncFun()

    def result = compare(hash(download('http://www.gpars.org')), hash(loadFile('/coolStuff/gpars/website/index.html')))

    println 'Allowed to do something else now'
    println "The result of comparison: " + result.get()

从异步函数内部调用异步函数

异步函数的另一个非常有价值的属性是 承诺 可以组合起来。


承诺可以组合起来!

一个异步函数位于另一个异步函数内部
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import static groovyx.gpars.GParsPool.withPool

  withPool {
      Closure plus = {Integer a, Integer b ->
          sleep 3000
          println 'Adding numbers'
          a + b
      }.asyncFun();        // ok, here's one func

      Closure multiply = {Integer a, Integer b ->
          sleep 2000
          a * b
      }.asyncFun()        // and second one

      Closure measureTime = {->
          sleep 3000
          4
      }.asyncFun();        // and another

          // declare a function within a function
      Closure distance = {Integer initialDistance, Integer velocity, Integer time ->
          plus(initialDistance, multiply(velocity, time))
      }.asyncFun();        // and another


      Closure chattyDistance = {Integer initialDistance, Integer velocity, Integer time ->
          println 'All parameters are now ready - starting'
          println 'About to call another asynchronous function'
          def innerResultPromise = plus(initialDistance, multiply(velocity, time))
          println 'Returning the promise for the inner calculation as my own result'
          return innerResultPromise
      }.asyncFun();        // and declare (but not run) a final asynch.function

          // fine, now let's execute those previous asynch. functions
      println "Distance = " + distance(100, 20, measureTime()).get() + ' m'
      println "ChattyDistance = " + chattyDistance(100, 20, measureTime()).get() + ' m'
  }

如果一个异步函数(例如本示例中的 distance 函数)在其主体中调用另一个异步函数(例如 plus )并返回调用函数的承诺,那么内部函数 ( plus ) 的结果承诺将与外部函数 ( distance ) 的结果承诺组合起来。

一旦内部函数 ( plus ) 完成计算,内部函数 ( plus ) 现在将把它的结果绑定到外部函数 ( distance ) 的承诺上。这种承诺组合逻辑的能力允许函数停止计算,而不会阻塞线程。这种情况不仅发生在等待參數时,而且在它们在代码主体中的任何位置调用另一个异步函数时也会发生。


方法作为异步函数

可以使用 .& 运算符将方法引用为闭包。然后可以使用 asyncFun 方法将这些闭包转换为可组合的异步函数,就像普通的闭包一样。

一个示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class DownloadHelper {

    String download(String url) {
        url.toURL().text
    }

    int scanFor(String word, String text) {
        text.findAll(word).size()
    }

    String lower(s) {
        s.toLowerCase()
    }
}

//now we'll make the methods asynchronous
withPool {
    final DownloadHelper d = new DownloadHelper()
    Closure download = d.&download.asyncFun()   // notice the .& syntax
    Closure scanFor = d.&scanFor.asyncFun()                // and here
    Closure lower = d.&lower.asyncFun()                        // and here

    //asynchronous processing
    def result = scanFor('groovy', lower(download('http://www.infoq.com')))
    println 'Doing something else for now'
    println result.get()
}

使用注解创建异步函数

与调用 asyncFun() 函数相比,可以使用 @AsyncFun 注解来注解 Closure 类型的字段。这些字段必须在原处初始化,并且包含类需要在 withPool 块中实例化。

一个注解示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import static groovyx.gpars.GParsPool.withPool
import groovyx.gpars.AsyncFun

class DownloadingSearch {
    @AsyncFun Closure download = {String url ->
        url.toURL().text
    }

    @AsyncFun Closure scanFor = {String word, String text ->
        text.findAll(word).size()
    }

    @AsyncFun Closure lower = {s -> s.toLowerCase()}

    void scan() {
        def result = scanFor('groovy', lower(download('http://www.infoq.com')))  //synchronous processing

        println 'Allowed to do something else now'
        println result.get()
    }
}

withPool {
    new DownloadingSearch().scan()
}
替代池

AsyncFun 注解默认使用来自包装 withPool 块的 GParsPool 实例。但是,你可以显式地指定池类型

一个显式示例
1
@AsyncFun(GParsExecutorsPoolUtil) def sum6 = {a, b -> a + b }
通过注解阻塞函数

AsyncFun 方法还允许我们指定结果函数应该允许阻塞 (true) 还是非阻塞 (false - 默认) 语义。

一个阻塞语义示例
1
2
@AsyncFun(blocking = true)
def sum = {a, b -> a + b }

显式和延迟的池分配

当直接使用 GPars(Executors)PoolUtil.asyncFun() 函数来创建异步函数时,你有两种额外的分配线程池到函数的方式。

  1. 可以在创建时作为附加参数显式地指定函数使用的线程池

  2. 可以在调用时从周围作用域隐式地获取线程池,而不是在创建时获取

当显式地指定线程池时,调用不需要包含在 withPool() 块中

显式地指定线程池
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Closure sPlus = {Integer a, Integer b ->
    a + b
}

Closure sMultiply = {Integer a, Integer b ->
    sleep 2000
    a * b
}

println "Synchronous result: " + sMultiply(sPlus(10, 30), 100)

final pool = new FJPool();

Closure aPlus = GParsPoolUtil.asyncFun(sPlus, pool)
Closure aMultiply = GParsPoolUtil.asyncFun(sMultiply, pool)

def result = aMultiply(aPlus(10, 30), 100)

println "Time to do something else while the calculation is running"
println "Asynchronous result: " + result.get()

使用延迟池分配,只有函数调用需要包含在 withPool() 块中

一个延迟池分配示例
1
2
3
4
5
6
7
8
9
Closure aPlus = GParsPoolUtil.asyncFun(sPlus)
Closure aMultiply = GParsPoolUtil.asyncFun(sMultiply)

withPool {
    def result = aMultiply(aPlus(10, 30), 100)

    println "Time to do something else while the calculation is running"
    println "Asynchronous result: " + result.get()
}

对我们来说,这是一个非常有趣的领域,值得探索。所以,欢迎任何关于组合异步函数的评论、问题或建议,或者关于其局限性的提示。


Fork-Join

Fork/Join分而治之 是一种非常强大的抽象,用于解决层次化问题。

抽象

当谈论层次化问题时,请考虑快速排序、归并排序、文件系统或一般树导航问题。

  • Fork/Join 算法本质上将一个问题分解为多个较小的子问题,然后递归地对每个子问题应用相同的算法。

  • 一旦子问题足够小,它就会被直接解决。

  • 所有子问题的解被组合起来解决它们的父问题,这反过来又帮助解决其自己的祖父母问题。

一张图胜过千言万语

查看这个花哨的交互式Fork/Join可视化演示。它向您展示了线程如何协同工作来解决常见的“分而治之”算法。

强大的JSR-166y库很好地协调了Fork/Join的编排,但留下了一些粗糙的边缘,如果您没有足够注意,这些边缘可能会伤害您。您仍然必须处理线程、池和/或同步屏障。

GPars抽象便利层

GPars可以为您隐藏处理线程、池和递归任务的复杂性,同时让您利用jsr166y中强大的Fork/Join实现。

一个用于遍历文件目录的复杂示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import static groovyx.gpars.GParsPool.runForkJoin
import static groovyx.gpars.GParsPool.withPool

withPool() {
    println """Number of files: ${

        runForkJoin(new File("./src")) {file ->
            long count = 0
            file.eachFile {
                if (it.isDirectory()) {
                    println "Forking a child task for $it"
                    forkOffChild(it)           //fork a child task

                } else {
                    count++
                }
            }
            return count + (childrenResults.sum(0))
            //use results of children tasks to calculate and store own result
        }

    }""".toString();
}

runForkJoin() 工厂方法使用提供的递归代码以及提供的值来构建一个层次化的Fork/Join计算。传递给runForkJoin()方法的值数量必须与闭包的预期参数数量匹配。这必须等于传递给forkOffChild()runChildDirectly()方法的参数数量。

一个示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def quicksort(numbers) {

    withPool {

        runForkJoin(0, numbers) {index, list ->

            def groups = list.groupBy {it <=> list[list.size().intdiv(2)]}

            if ((list.size() < 2) || (groups.size() == 1)) {
                return [index: index, list: list.clone()]
            }

            (-1..1).each {forkOffChild(it, groups[it] ?: [])}

            return [index: index, list: childrenResults.sort {it.index}.sum {it.list}]

        }.list
    }
}
它是异步的,伙计!

这里需要注意的关键是forkOffChild()不会等待子任务运行。它只是安排在将来的某个时间执行它。如果子任务抛出异常,不要期望异常从forkOffChild()方法本身抛出。异常将在父进程调用forkOffChild()很久之后发生。

getChildrenResults()方法将任何子任务异常重新抛回父进程。

另一种方法

或者,可以直接使用嵌套Fork/Join工作任务的底层机制。定制的工作者可以消除使用通用工作者时参数扩展带来的性能开销。

此外,还可以用Java实现自定义工作者,以进一步提高性能。

一个自定义工作者
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public final class FileCounter extends AbstractForkJoinWorker<Long> {
    private final File file;

    def FileCounter(final File file) {
        this.file = file
    }

    @Override
    protected Long computeTask() {
        long count = 0;

        file.eachFile {
            if (it.isDirectory()) {
                println "Forking a thread for $it"
                forkOffChild(new FileCounter(it))           //fork a child task

            } else {
                count++
            }
        }
        return count + ((childrenResults)?.sum() ?: 0)  //use results of children tasks to calculate and store own result
    }
}

withPool(1) {pool ->  //feel free to experiment with the number of fork/join threads in the pool
    println "Number of files: ${runForkJoin(new FileCounter(new File("..")))}"
}

AbstractForkJoinWorker子类可以用JavaGroovy编写。无论哪种选择都可以让您优化执行速度,如果工作者的低性能成为瓶颈。

Fork / Join 节省您的资源

由于内部使用了TaskBarrier类来同步线程,因此Fork/Join操作可以在少量线程的情况下安全运行。

当一个线程在算法内部被阻塞,等待其子问题的计算时,该线程会静默地返回到其池中,从任务队列中接管任何其他可用的子问题并处理它们。尽管该算法会为每个子目录创建与子目录数量相同的任务,并且任务会等待子目录任务完成,但通常只需要一个线程就足以保持计算持续进行,并最终计算出有效结果。

归并排序示例

来吧,朋克,帮我合并一下!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
import static groovyx.gpars.GParsPool.runForkJoin
import static groovyx.gpars.GParsPool.withPool

/**
 * Splits a list of numbers in half
 */
def split(List<Integer> list) {
    int listSize = list.size()
    int middleIndex = listSize / 2
    def list1 = list[0..<middleIndex]
    def list2 = list[middleIndex..listSize - 1]
    return [list1, list2]
}

/**
 * Merges two sorted lists into one
 */
List<Integer> merge(List<Integer> a, List<Integer> b) {
    int i = 0, j = 0
    final int newSize = a.size() + b.size()
    List<Integer> result = new ArrayList<Integer>(newSize)

    while ((i < a.size()) && (j < b.size())) {
        if (a[i] <= b[j]) result << a[i++]
        else result << b[j++]
    }

    if (i < a.size()) result.addAll(a[i..-1])
    else result.addAll(b[j..-1])
    return result
}

final def numbers = [1, 5, 2, 4, 3, 8, 6, 7, 3, 4, 5, 2, 2, 9, 8, 7, 6, 7, 8, 1, 4, 1, 7, 5, 8, 2, 3, 9, 5, 7, 4, 3]

withPool(3) {  //feel free to experiment with the number of fork/join threads in the pool
    println """Sorted numbers: ${
        runForkJoin(numbers) {nums ->
            println "Thread ${Thread.currentThread().name[-1]}: Sorting $nums"
            switch (nums.size()) {
                case 0..1:
                    return nums                                   //store own result
                case 2:
                    if (nums[0] <= nums[1]) return nums     //store own result
                    else return nums[-1..0]                       //store own result
                default:
                    def splitList = split(nums)
                    [splitList[0], splitList[1]].each {forkOffChild it}  //fork a child task
                    return merge(* childrenResults)      //use results of children tasks to calculate and store own result
            }
        }
    }"""
}

使用定制的工作者类的归并排序示例

一个示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
public final class SortWorker extends AbstractForkJoinWorker<List<Integer>> {
    private final List numbers

    def SortWorker(final List<Integer> numbers) {
        this.numbers = numbers.asImmutable()
    }

    /**
     * Splits a list of numbers in half
     */
    def split(List<Integer> list) {
        int listSize = list.size()
        int middleIndex = listSize / 2
        def list1 = list[0..<middleIndex]
        def list2 = list[middleIndex..listSize - 1]
        return [list1, list2]
    }

    /**
     * Merges two sorted lists into one
     */
    List<Integer> merge(List<Integer> a, List<Integer> b) {
        int i = 0, j = 0
        final int newSize = a.size() + b.size()

        List<Integer> result = new ArrayList<Integer>(newSize)

        while ((i < a.size()) && (j < b.size())) {
            if (a[i] <= b[j]) result << a[i++]
            else result << b[j++]
        }

        if (i < a.size()) result.addAll(a[i..-1])
        else result.addAll(b[j..-1])
        return result
    }

    /**
     * Sorts a small list or delegates to two children, if the list contains more than two elements.
     */
    @Override
    protected List<Integer> computeTask() {
        println "Thread ${Thread.currentThread().name[-1]}: Sorting $numbers"

        switch (numbers.size()) {
            case 0..1:
                return numbers                                   //store own result

            case 2:
                if (numbers[0] <= numbers[1]) return numbers     //store own result
                else return numbers[-1..0]                       //store own result

            default:
                def splitList = split(numbers)
                [new SortWorker(splitList[0]), new SortWorker(splitList[1])].each{forkOffChild it}  //fork a child task
                return merge(* childrenResults)      //use results of children tasks to calculate and store own result
        }
    }
}

final def numbers = [1, 5, 2, 4, 3, 8, 6, 7, 3, 4, 5, 2, 2, 9, 8, 7, 6, 7, 8, 1, 4, 1, 7, 5, 8, 2, 3, 9, 5, 7, 4, 3]

withPool(1) {  //feel free to experiment with the number of fork/join threads in the pool
    println "Sorted numbers: ${runForkJoin(new SortWorker(numbers))}"
}

直接运行子任务

forkOffChild方法有一个兄弟姐妹——叫做runChildDirectly方法。此方法将直接在当前线程中立即运行子任务,而不是将子任务安排在线程池中进行异步处理。通常,您会在每个子任务上调用forkOffChild,但最后一个子任务除外,您会直接调用它,而不会进行调度开销。

早点分叉,节省九次
1
2
3
4
5
6
7
8
9
10
11
12
13
Closure fib = {number ->
    if (number <= 2) {
        return 1
    }

    forkOffChild(number - 1)  // This task will run asynchronously, probably in a different thread
    final def result = runChildDirectly(number - 2)     //  This task is run directly within the current thread
    return (Integer) getChildrenResults().sum() + result
}

withPool {
    assert 55 == runForkJoin(10, fib)
}

可用性

此功能仅在使用基于Fork/JoinGParsPool时可用,而不能在GParsExecutorsPool中使用。


并行推测

随着处理器内核变得越来越丰富,某些算法可能会从暴力并行复制中受益。您不需要事先决定如何解决问题、使用哪种算法或连接到哪个位置,而是可以并行运行所有潜在的解决方案。

并行推测

假设您需要执行一项任务,例如计算一个昂贵的函数或从文件、数据库或互联网读取数据。幸运的是,您知道几种很好的方法(例如函数或 URL)来实现目标。然而,并非所有方法都一样好。

尽管它们返回相同的结果(就您的需求而言),但每个方法的经过时间会有所不同,有些方法甚至可能失败(例如网络问题)。更糟糕的是,没有人会告诉你哪种选择能给你最好的解决方案,以及哪些路径可能根本没有解决方案。

  1. 我应该在列表上运行快速排序还是归并排序

  2. 哪个 URL 最好用?

  3. 该服务在其主位置可用吗?还是我应该使用备份位置?

GPars 推测可以让您选择并行尝试所有可用的备选方案,并从最快的功能路径接收结果,静默地忽略缓慢或损坏的路径。

这是GParsPoolGParsExecutorsPool上的speculate方法可以为您做到的。

一个排序示例
1
2
3
4
def numbers = ...
def quickSort = ...
def mergeSort = ...
def sortedNumbers = speculate(quickSort, mergeSort)

因此,我们同时(并发地)执行快速排序归并排序,同时获取更快排序的结果。

鉴于当今主流硬件上可用的并行资源,并行运行这两个函数不会对任何一个函数的计算速度产生重大影响,因此我们在大约与仅运行两个计算中较快的那一个相同的时间内获得了两个函数的结果。此外,结果也比运行较慢的那个函数来得早。然而,我们不需要事先知道哪种排序算法在我们的数据上表现更好。因此我们进行了推测(猜测)。

同样,从多个具有不同速度和/或可靠性的来源下载文档可能看起来像这样

一个文档下载示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import static groovyx.gpars.GParsPool.speculate
import static groovyx.gpars.GParsPool.withPool

def alternative1 = {
    'http://www.dzone.com/links/index.html'.toURL().text
}

def alternative2 = {
    'http://www.dzone.com/'.toURL().text
}

def alternative3 = {
    'http://www.dzzzzzone.com/'.toURL().text  //wrong url
}

def alternative4 = {
    'http://dzone.com/'.toURL().text
}

withPool(4){
    println speculate([alternative1, alternative2, alternative3, alternative4]).contains('groovy')
}
线程饥饿

确保周围的线程池有足够的线程来并行处理所有备选方案。池的大小应与提供的闭包数量匹配。

使用 数据流变量 的替代方案

在某些用例中,我们可以忽略失败的备选方案,因此可以使用数据流变量或来获取获胜推测的结果。

请参阅本用户指南中的数据流并发主题

请参阅本用户指南中的数据流并发部分,了解有关数据流变量和流的详细信息。

一个示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import groovyx.gpars.dataflow.DataflowQueue
import static groovyx.gpars.dataflow.Dataflow.task

def alternative1 = {
    'http://www.dzone.com/links/index.html'.toURL().text
}

def alternative2 = {
    'http://www.dzone.com/'.toURL().text
}

def alternative3 = {
    'http://www.dzzzzzone.com/'.toURL().text  //will fail due to wrong url
}

def alternative4 = {
    'http://dzone.com/'.toURL().text
}

//Pick either one of the following, both will work:
final def result = new DataflowQueue()
//  final def result = new DataflowVariable()

[alternative1, alternative2, alternative3, alternative4].each{code ->
    task{
        try {
            result << code()
        }
        catch (ignore) { }  // We deliberately ignore unsuccessful urls.
    }
}

println result.val.contains('groovy')

divider

用户指南:CSP

通信顺序进程

CSP(通信顺序进程)抽象建立在独立的可组合进程之上,这些进程以同步的方式交换消息。GPars 利用英国肯特大学开发的JCSP

Jon KerridgeGParsCSP实现的作者,他在www.soc.napier.ac.uk我们本地镜像页面上的此处提供了GroovyCSP使用的详尽示例。

目的

GroovyCSP实现利用JCSP,一个基于 Java 的CSP库,该库在 LGPL 下授权。Apache 2 许可证(GPars 使用的许可证)和 LGPL 之间存在一些差异。在您的代码中启用JCSP的使用之前,请确保您的应用程序符合 LGPL 规则。

如果 LGPL 许可证不适合您的使用,您可以考虑查看本用户指南中的数据流并发章节,以了解任务选择器操作符,它们可以帮助您以类似于CSP方法的方式解决并发问题。事实上,GPars中实现的数据流和CSP概念彼此非常接近。

Apache 2 许可证

默认情况下,如果您没有在您的构建文件中主动添加对JCSP的显式依赖关系,或者没有下载并在您的项目中包含JCSP jar 文件,则标准的商业软件友好的Apache 2 许可证条款适用于您的项目。GPars直接只依赖于在与Apache 2 许可证兼容的许可证下授权的软件。

CSP 模型原则

本质上,CSP模型建立在独立的并发进程之上,这些进程通过使用同步(即会合)消息传递的通道相互通信。与围绕事件处理模式旋转的参与者或数据流操作符不同,CSP进程将其活动(又名步骤序列)的重点放在使用通信上,以保持在整个过程中相互同步。

由于寻址是通过通道间接进行的,因此进程不需要了解彼此。它们通常由一组输入和输出通道以及一个主体组成。一旦CSP进程启动,它就会从线程池中获取一个线程并开始处理其主体,只有在从通道读取或写入通道时才会暂停。某些实现(例如GoLang)还可以将线程从CSP进程中分离,当它在通道上阻塞时。

CSP 程序是确定性的。程序输入上的相同数据将始终生成相同的输出,无论使用的实际线程调度方案如何。这在调试CSP程序以及分析死锁时非常有帮助。

确定性与间接寻址相结合,在CSP进程的可组合性方面取得了很高的水平。您可以通过连接其输入和输出通道,然后将它们包装在另一个更大的包含进程中,将小的CSP进程组合成更大的进程。

CSP模型使用备选方案引入了不确定性。进程可以通过称为备选方案选择的结构,同时尝试从多个通道读取值。在选择中涉及的任何通道中第一个可用的值将被读取并被进程消耗。由于通过选择接收到的消息的顺序取决于程序运行时的不可预测条件,因此将读取的值是不确定的。


GPars 数据流与 CSP

GPars提供了创建CSP进程所需的所有构建块。

  • CSP进程可以使用GPars任务通过闭包RunnableCallable来建模,这些任务用于保存进程的实际实现

  • CSP 通道应该使用SyncDataflowQueueSyncDataflowBroadcast类来建模

  • CSP 备选方案通过Select类及其selectprioritySelect方法提供


进程

要启动一个进程,只需使用task工厂方法。

启动一个进程
1
2
3
4
5
6
7
8
9
10
import groovyx.gpars.group.DefaultPGroup
import groovyx.gpars.scheduler.ResizeablePool

group = new DefaultPGroup(new ResizeablePool(true))

def t = group.task {
    println "I am a process"
}

t.join()
由于每个进程在其生命周期中都会消耗一个线程,因此建议使用可调整大小的线程池,如上例所示。

也可以从RunnableCallable对象创建进程

一个 Runnable 示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import groovyx.gpars.group.DefaultPGroup
import groovyx.gpars.scheduler.ResizeablePool

group = new DefaultPGroup(new ResizeablePool(true))

class MyProcess implements Runnable {

    @Override
    void run() {
        println "I am a process"
    }
}
def t = group.task new MyProcess()

t.join()

使用Callable允许通过get()方法返回值

一个 Callable 示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import groovyx.gpars.group.DefaultPGroup
import groovyx.gpars.scheduler.ResizeablePool

import java.util.concurrent.Callable

group = new DefaultPGroup(new ResizeablePool(true))

class MyProcess implements Callable<String> {

    @Override
    String call() {
        println "I am a process"
        return "CSP is great!"
    }
}

def t = group.task new MyProcess()

println t.get()

通道

进程通常需要通道来与其伴侣进程以及外部世界进行通信

一个通道示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import groovy.transform.TupleConstructor
import groovyx.gpars.dataflow.DataflowReadChannel
import groovyx.gpars.dataflow.DataflowWriteChannel
import groovyx.gpars.group.DefaultPGroup
import groovyx.gpars.scheduler.ResizeablePool

import java.util.concurrent.Callable
import groovyx.gpars.dataflow.SyncDataflowQueue

group = new DefaultPGroup(new ResizeablePool(true))

@TupleConstructor
class Greeter implements Callable<String> {
    DataflowReadChannel names
    DataflowWriteChannel greetings

    @Override
    String call() {
        while(!Thread.currentThread().isInterrupted()) {
            String name = names.val
            greetings << "Hello " + name
        }
        return "CSP is great!"
    }
}

def a = new SyncDataflowQueue()
def b = new SyncDataflowQueue()

group.task new Greeter(a, b)

a << "Joe"
a << "Dave"
println b.val
println b.val
使用哪种传递技术来传递消息?

CSP模型使用同步消息传递,但在GPars中,您也可以考虑使用异步通道以及同步通道。

您还可以在同一个进程中组合这两种类型的通道。

组合

将进程分组仅仅是通过通道将它们连接起来的问题

一个分组示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
group = new DefaultPGroup(new ResizeablePool(true))

@TupleConstructor
class Formatter implements Callable<String> {
    DataflowReadChannel rawNames
    DataflowWriteChannel formattedNames

    @Override
    String call() {
        while(!Thread.currentThread().isInterrupted()) {
            String name = rawNames.val
            formattedNames << name.toUpperCase()
        }
    }
}

@TupleConstructor
class Greeter implements Callable<String> {
    DataflowReadChannel names
    DataflowWriteChannel greetings

    @Override
    String call() {
        while(!Thread.currentThread().isInterrupted()) {
            String name = names.val
            greetings << "Hello " + name
        }
    }
}

def a = new SyncDataflowQueue()
def b = new SyncDataflowQueue()
def c = new SyncDataflowQueue()

group.task new Formatter(a, b)
group.task new Greeter(b, c)

a << "Joe"
a << "Dave"
println c.val
println c.val

替代方案

为了引入非确定性,GPars 提供了Select 类及其selectprioritySelect 方法。

Select 示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import groovy.transform.TupleConstructor
import groovyx.gpars.dataflow.SyncDataflowQueue
import groovyx.gpars.dataflow.DataflowReadChannel
import groovyx.gpars.dataflow.DataflowWriteChannel
import groovyx.gpars.dataflow.Select
import groovyx.gpars.group.DefaultPGroup
import groovyx.gpars.scheduler.ResizeablePool

import static groovyx.gpars.dataflow.Dataflow.select

group = new DefaultPGroup(new ResizeablePool(true))

@TupleConstructor
class Receptionist implements Runnable {
    DataflowReadChannel emails
    DataflowReadChannel phoneCalls
    DataflowReadChannel tweets
    DataflowWriteChannel forwardedMessages

    private final Select incomingRequests = select([phoneCalls, emails, tweets])  //prioritySelect() would give highest precedence to phone calls

    @Override
    void run() {
        while(!Thread.currentThread().isInterrupted()) {
            String msg = incomingRequests.select()
            forwardedMessages << msg.toUpperCase()
        }
    }
}

def a = new SyncDataflowQueue()
def b = new SyncDataflowQueue()
def c = new SyncDataflowQueue()
def d = new SyncDataflowQueue()

group.task new Receptionist(a, b, c, d)

a << "my email"
b << "my phone call"
c << "my tweet"

//The values come in random order since the process uses a Select to read its input
3.times{
    println d.val.value
}

组件

CSP 进程可以组合成更大的实体。假设您已经有一组 CSP 进程(又名Runnable/Callable 类),您可以将它们组合成一个更大的进程。

更大规模的示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
final class Prefix implements Callable {
    private final DataflowChannel inChannel
    private final DataflowChannel outChannel
    private final def prefix

    def Prefix(final inChannel, final outChannel, final prefix) {
        this.inChannel = inChannel;
        this.outChannel = outChannel;
        this.prefix = prefix
    }

    public def call() {
        outChannel << prefix
        while (true) {
            sleep 200
            outChannel << inChannel.val
        }
    }
}
另一个构建块
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
final class Copy implements Callable {
    private final DataflowChannel inChannel
    private final DataflowChannel outChannel1
    private final DataflowChannel outChannel2

    def Copy(final inChannel, final outChannel1, final outChannel2) {
        this.inChannel = inChannel;
        this.outChannel1 = outChannel1;
        this.outChannel2 = outChannel2;
    }

    public def call() {
        final PGroup group = Dataflow.retrieveCurrentDFPGroup()
        while (true) {
            def i = inChannel.val
            group.task {
                outChannel1 << i
                outChannel2 << i
            }.join()
        }
    }
}
示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import groovyx.gpars.dataflow.DataflowChannel
import groovyx.gpars.dataflow.SyncDataflowQueue
import groovyx.gpars.group.DefaultPGroup

group = new DefaultPGroup(6)

def fib(DataflowChannel out) {
    group.task {
        def a = new SyncDataflowQueue()
        def b = new SyncDataflowQueue()
        def c = new SyncDataflowQueue()
        def d = new SyncDataflowQueue()
        [new Prefix(d, a, 0L), new Prefix(c, d, 1L), new Copy(a, b, out), new StatePairs(b, c)].each { group.task it}
    }
}

final SyncDataflowQueue ch = new SyncDataflowQueue()
group.task new Print('Fibonacci numbers', ch)
fib(ch)

sleep 10000

divider

用户指南:演员

Actor 提供了基于消息传递的并发模型:程序是独立的活动对象集合,这些对象交换消息并且没有可变的共享状态。

Actor 可以帮助我们避免死锁、活锁和饥饿等问题,这些问题是基于共享内存的方法的常见问题。

Actor 是利用当今硬件的多核性质而无需解决传统上与共享内存多线程相关的所有问题的一种方式,这就是为什么诸如 ErlangScala 之类的编程语言采用了这种模型。


GPars 中的 actor 支持最初受 Scala 中的 Actor 库启发,但此后已远远超出了 Scala 作为标准提供的功能。

Ruben Vermeersch 撰写了一篇很好的文章,总结了actor 背后的关键概念

Actor 始终保证最多只有一个线程在任何时候处理 actor 的主体,并且在幕后,每次将线程分配给 actor 时都会同步内存,因此 actor 的状态可以通过主体中的代码安全地修改无需任何其他额外(同步或锁定)工作

理想情况下,actor 的代码应该从外部直接调用,因此 actor 类的所有代码只能由处理最后一个接收到的消息的线程执行,因此所有 actor 的代码都是隐式线程安全的

如果允许其他对象直接调用 actor 的任何方法,则 actor 代码和状态的线程安全保证将不再有效


演员类型

通常,您会在野外找到两种类型的 actor——具有隐式状态的 actor 和没有隐式状态的 actor。

GPars 为您提供了两种选择。

无状态 actor,在 GPars 中由DynamicDispatchActorReactiveActor 类表示,不会跟踪之前到达的消息。您可以将它们视为平面消息处理程序,它们按顺序处理消息。任何基于状态的行为都必须由用户实现。

有状态 actor,在 GPars 中由DefaultActor 类表示(以前也由AbstractPooledActor 类表示),允许我们直接处理隐式状态。在收到消息后,actor 将进入一个新的状态,该状态具有不同的方式来处理未来的消息。

举个例子,一个刚启动的 actor 可能只接受某些类型的消息,例如,只有在收到加密密钥后才能接受用于解密的加密消息。有状态 actor 允许将这种依赖关系直接编码到消息处理代码的结构中。但是,隐式状态管理会带来轻微的性能成本,这主要是因为 JVM 不支持延续。

演员线程模型

由于 actor 与系统线程分离,因此大量 actor 可以共享一个相对较小的线程池。

这可以扩展到让许多并发 actor 共享一个单一的池化线程,同时避免 JVM 的一些线程限制。

通常,虽然 JVM 只能为您提供有限数量的线程(通常大约几千个),但 actor 的数量仅受可用内存的限制。如果 actor 没有工作要做,它不会消耗任何线程。

actor 代码以块的形式处理,这些块由等待新事件(消息)的安静期隔开。这可以通过延续自然地建模。

由于 JVM 不直接支持延续,因此必须在 actor 框架中模拟它们,这对 actor 代码的组织有轻微的影响。但是,在大多数情况下,益处超过了困难。


Actor 示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
import groovyx.gpars.actor.Actor
import groovyx.gpars.actor.DefaultActor

class GameMaster extends DefaultActor {
    int secretNum

    void afterStart() {
        secretNum = new Random().nextInt(10)
    }

    void act() {
        loop {
            react { int num ->
                if (num > secretNum) {
                    reply 'too large'
                }
                else if (num < secretNum) {
                    reply 'too small'
                }
                else {
                    reply 'you win'
                    terminate()
                }
            }
        }
    }
}

class Player extends DefaultActor {
    String name
    Actor server
    int myNum

    void act() {
        loop {
            myNum = new Random().nextInt(10)
            server.send myNum
            react {
                switch (it) {
                  case 'too large': println "$name: $myNum was too large"; break
                  case 'too small': println "$name: $myNum was too small"; break
                  case 'you win': println "$name: I won $myNum"; terminate(); break
                }
            }
        }
    }
}

def master = new GameMaster().start()
def player = new Player(name: 'Player', server: master).start()

// This forces the main thread to wait until both actors have terminated.
[master, player]*.join()

Jordi Campos i Miralles, Departament de Matemàtica Aplicada i Anàlisi, MAiA Facultat de Matemàtiques, Universitat de Barcelona提供示例


演员的用法

GPars 提供一致的 Actor API 和 DSL。原则上,actor 执行三个特定操作——发送消息、接收消息和创建新 actor。虽然 GPars 没有特别强制执行,但消息应该是不可变的,或者至少在发送者在发送消息后不再触碰消息的情况下遵循hands-off 策略。

发送消息

可以使用send 方法将消息发送到 actor。

示例
1
2
3
4
5
6
7
8
def passiveActor = Actors.actor{
    loop {
        react { msg -> println "Received: $msg"; }
    }
}
passiveActor.send 'Message 1'
passiveActor << 'Message 2'    //using the << operator
passiveActor 'Message 3'       //using the implicit call() method

或者,可以使用<< 运算符或隐式call 方法。提供了一系列sendAndWait 方法来阻塞调用方,直到收到来自 actor 的回复。replysendAndWait 方法中作为返回值返回。sendAndWait 方法也可能在超时到期或被调用 actor 终止时返回。

示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def replyingActor = Actors.actor{
    loop {
        react { msg ->
            println "Received: $msg";
            reply "I've got $msg"
        }
    }
}

def reply1 = replyingActor.sendAndWait('Message 4')

def reply2 = replyingActor.sendAndWait('Message 5', 10, TimeUnit.SECONDS)

use (TimeCategory) {
    def reply3 = replyingActor.sendAndWait('Message 6', 10.seconds)
}

sendAndContinue 方法允许调用方在提供的闭包等待来自 actor 的回复时继续其处理。

示例
1
2
friend.sendAndContinue 'I need money!', {money -> pocket money}
println 'I can continue while my friend is collecting money for me'

sendAndPromise 方法返回对最终回复的Promise(又名 Future),因此允许调用方在 actor 处理提交的消息时继续其处理。

示例
1
2
3
4
Promise loan = friend.sendAndPromise 'I need money!'
println 'I can continue while my friend is collecting money for me'
loan.whenBound {money -> pocket money}  // Asynchronous waiting for a reply.
println "Received ${loan.get()}"  // Synchronous waiting for a reply.

所有sendsendAndWaitsendAndContinue 方法如果在非活动 actor 上调用都会抛出异常。


接收消息

非阻塞消息检索

从 actor 代码中调用react 方法(可选地使用超时参数)将从 actor 的收件箱中消耗下一条消息,如果立即没有要处理的消息,可能会等待。

示例
1
2
3
4
println 'Waiting for a gift'
react {gift ->
    if (mySpouse.likes gift) reply 'Thank you!'
}

在幕后,提供的闭包不会直接调用,而是由线程池中的任何线程在消息可用时调度处理。调度后,当前线程将与 actor 分离并释放以处理任何其他已收到消息的 actor。

为了允许将 actor 与线程分离,react 方法要求以特殊的延续风格编写代码。

react 示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Actors.actor {
    loop {
        println 'Waiting for a gift'
        react {gift ->
            if (mySpouse.likes gift) reply 'Thank you!'
            else {
                reply 'Try again, please'
                react {anotherGift ->
                    if (myChildren.like gift) reply 'Thank you!'
                }
                println 'Never reached'
            }
        }
        println 'Never reached'
    }
    println 'Never reached'
}

react 方法具有特殊的语义,允许 actor 在其邮箱中没有消息可用时与线程分离。本质上,react 将提供的代码(闭包)调度在下一条消息到达时执行并返回。提供给react 方法的闭包是计算应该恢复的代码。这是一种延续风格

由于 actor 必须保持保证,即最多只有一个线程在 actor 的主体中处于活动状态,因此在当前消息处理完成之前不能处理下一条消息。通常,不需要在调用react 之后放置代码。某些 actor 实现甚至强制执行此操作。但是,出于性能原因,GPars 不会这样做。loop 方法允许在 actor 主体中进行迭代。与传统的循环结构(如forwhile 循环)不同,loop 与嵌套的react 块协作,并确保跨随后的消息检索进行循环。

发送回复

replyreplyIfExists 方法不仅在 actor 本身上定义,而且对于AbstractPooledActor(在DefaultActorDynamicDispatchActorReactiveActor 类中不可用)也定义在接收到的消息本身中,这在处理单个调用中的多个消息时尤其方便。在这种情况下,在 actor 上调用的reply() 将向所有当前处理的消息(最后一个)的作者发送回复,而调用消息上的reply() 仅向该特定消息的作者发送回复。

请参阅我们此处示例演示中的 DemoMultiMessage.groovy
发送者属性

检索到的消息提供发送者属性以标识消息的发出者。该属性在 Actor 的闭包中可用。

示例
1
2
3
4
react {tweet ->
    if (isSpam(tweet)) ignoreTweetsFrom sender
    sender.send 'Never write to me again!'
}

转发

发送消息时,可以指定另一个 actor 作为发送者,以便对消息的潜在回复将转发到指定的 actor,而不是转发到实际的发出者。

示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def decryptor = Actors.actor {
    react {message ->
        reply message.reverse()
//      sender.send message.reverse()    //An alternative way to send replies
    }
}

def console = Actors.actor {  //This actor will print out decrypted messages, since the replies are forwarded to it
    react {
        println 'Decrypted message: ' + it
    }
}

decryptor.send 'lellarap si yvoorG', console  //Specify an actor to send replies to
console.join()

创建 Actor

Actor 共享一个线程,这些线程在 actor 需要发送给它们的**消息**做出反应时动态分配给 actor。当处理完一条消息并且 actor 处于空闲状态,等待更多消息到达时,线程将返回到池中。

例如,以下是如何创建一个 actor,该 actor 打印出它收到的所有消息。

Actor 示例
1
2
3
4
5
6
7
def console = Actors.actor {
    loop {
        react {
            println it
        }
    }
}

注意loop() 方法调用,它确保 actor 在处理完第一个消息后不会停止。

以下是一个带有解密服务的示例,该服务可以解密提交的消息并将解密的消息发送回发出者。

示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
final def decryptor = Actors.actor {
    loop {
        react {String message ->
            if ('stopService' == message) {
                println 'Stopping decryptor'
                stop()
            }
            else reply message.reverse()
        }
    }
}

Actors.actor {
    decryptor.send 'lellarap si yvoorG'
    react {
        println 'Decrypted message: ' + it
        decryptor.send 'stopService'
    }
}.join()

以下是一个 actor 的示例,它等待最多 30 秒以接收对其消息的回复。

示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
def friend = Actors.actor {
    react {
        //this doesn't reply -> caller won't receive any answer in time
        println it
        //reply 'Hello' //uncomment this to answer conversation
        react {
            println it
        }
    }
}

def me = Actors.actor {
    friend.send('Hi')
    //wait for answer 1sec
    react(1000) {msg ->
        if (msg == Actor.TIMEOUT) {
            friend.send('I see, busy as usual. Never mind.')
            stop()
        } else {
            //continue conversation
            println "Thank you for $msg"
        }
    }
}

me.join()

未送达消息

有时消息无法传递到目标 actor。当需要对未送达消息采取特殊操作时,在 actor 终止时,其队列中所有未处理的消息都将调用其onDeliveryError() 方法。在消息上定义的onDeliveryError() 方法或闭包可以例如将通知发送回消息的原始发送者。

处理未送达消息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
final DefaultActor me
me = Actors.actor {
    def message = 1

    message.metaClass.onDeliveryError = {->
        //send message back to the caller
        me << "Could not deliver $delegate"
    }

    def actor = Actors.actor {
        react {
            //wait 2sec in order next call in demo can be emitted
            Thread.sleep(2000)
            //stop actor after first message
            stop()
        }
    }

    actor << message
    actor << message

    react {
        //print whatever comes back
        println it
    }

}

me.join()

或者,可以在发送者本身指定onDeliveryError() 方法。该方法可以动态添加

动态示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
final DefaultActor me
me = Actors.actor {
    def message1 = 1
    def message2 = 2

    def actor = Actors.actor {
        react {
            //wait 2sec in order next call in demo can be emitted
            Thread.sleep(2000)
            //stop actor after first message
            stop()
        }
    }

    me.metaClass.onDeliveryError = {msg ->
        //callback on actor inaccessibility
        println "Could not deliver message $msg"
    }

    actor << message1
    actor << message2

    actor.join()

}

me.join()

以及在 actor 定义中静态添加

静态示例
1
2
3
4
5
6
class MyActor extends DefaultActor {
    public void onDeliveryError(msg) {
        println "Could not deliver message $msg"
    }
    ...
}

加入 Actor

Actor 提供join() 方法,允许调用方等待 actor 终止。还提供了一个接受超时的变体。Groovyspread-dot(*.)运算符在一次加入多个 actor 时很方便。

加入 Actor 的示例
1
2
3
4
def master = new GameMaster().start()
def player = new Player(name: 'Player', server: master).start()

[master, player]*.join()
条件循环和计数循环

loop() 方法允许指定条件或迭代次数,可选地附带一个闭包,以便在循环完成后调用该闭包——循环终止后的代码处理程序

以下 actor 将循环三次以接收 3 条消息,然后打印出接收到的消息的最大值。

示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
final Actor actor = Actors.actor {
    def candidates = []
    def printResult = {-> println "The best offer is ${candidates.max()}"}

    loop(3, printResult) {
        react {
            candidates << it
        }
    }
}

actor 10
actor 30
actor 20
actor.join()

以下 actor 将接收消息,直到收到大于 30 的值。

示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
final Actor actor = Actors.actor {
    def candidates = []
    final Closure printResult = {-> println "Reached best offer - ${candidates.max()}"}

    loop({-> candidates.max() < 30}, printResult) {
        react {
            candidates << it
        }
    }
}

actor 10
actor 20
actor 25
actor 31
actor 20
actor.join()

循环终止后的代码处理程序可以使用 actor 的react{},但不能使用loop()
公平与非公平 Actor 行为

DefaultActor 可以设置为以公平或非公平(默认)方式运行。根据选择的策略,actor 或者使线程可用于共享同一个并行组的其他 actor(公平),或者将线程保留给自己,直到消息队列为空(非公平)。通常,非公平 actor 的性能比公平 actor 高 2-3 倍。

使用fairActor() 工厂方法或 actor 的makeFair() 方法。

自定义调度程序

默认情况下,Actor 会利用标准 JDK 并发库。要提供自定义线程调度器,请在创建并行组 (**PGroup** 类) 时使用相应的构造函数参数。提供的调度器将编排组线程池中的线程。

另请参阅众多 Actor 示例演示程序。


演员原则

Actor 共享一个 **线程池**,当 Actor 需要 **响应** 发送给它们的 消息时,这些线程会被动态分配给 Actor。一旦消息被处理,Actor 处于空闲状态等待更多消息到达,线程就会返回到池中。Actor 与底层线程分离,因此一个相对较小的线程池可以服务于潜在的无限数量的 Actor。Actor 数量的无限可扩展性是 *基于事件的 Actor* 的主要优势,它与底层物理线程分离。

以下是一些使用 Actor 的示例。以下是如何创建一个 Actor 来打印它接收到的所有消息。

示例
1
2
3
4
5
6
7
8
import static groovyx.gpars.actor.Actors.actor

def console = actor {
    loop {
        react {
            println it
        }
    }

注意loop() 方法调用,它确保 actor 在处理完第一个消息后不会停止。

或者,您可以扩展 *DefaultActor* 类并覆盖 *act()* 方法。一旦实例化 Actor,您需要启动它,以便它将自己附加到线程池并开始接受消息。*actor()* 工厂方法将负责启动 Actor。

示例
1
2
3
4
5
6
7
8
9
10
11
12
13
class CustomActor extends DefaultActor {
    @Override
    protected void act() {
        loop {
            react {
                println it
            }
        }
    }
}

def console=new CustomActor()
console.start()

可以使用多种方法将消息发送给 Actor

示例
1
2
3
4
console.send('Message')
console 'Message'
console.sendAndWait 'Message'                                                     //Wait for a reply
console.sendAndContinue 'Message', {reply -> println "I received reply: $reply"}  //Forward the reply to a function

创建异步服务

示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import static groovyx.gpars.actor.Actors.actor

final def decryptor = actor {
    loop {
        react {String message->
            reply message.reverse()
        }
    }
}

def console = actor {
    decryptor.send 'lellarap si yvoorG'
    react {
        println 'Decrypted message: ' + it
    }
}

console.join()

如您所见,您使用 *actor()* 方法创建新的 Actor,并将 Actor 的主体作为闭包参数传入。在 Actor 的主体中,您可以使用 *loop()* 进行迭代,使用 *react()* 接收消息,使用 *reply()* 向发送当前处理消息的 Actor 发送消息。当前消息的发送者也可以通过 Actor 的 *sender* 属性获得。当解密器 Actor 在调用 *react()* 时在其消息队列中找不到消息时,*react()* 方法会放弃线程并将其返回到线程池,供其他 Actor 接收。

只有当 Actor 的消息队列收到新消息后,才会将 *react()* 方法的闭包安排到池中进行处理。基于事件的 Actor 在内部模拟了延续 - Actor 的工作 - 被分割成按顺序运行的块,一旦邮箱中有消息可用,这些块就会被调用。单个 Actor 的每个块都可以由线程池中的不同线程执行。

**Groovy** 的灵活语法和闭包允许我们的库提供多种定义 Actor 的方法。例如,以下是一个 Actor 的示例,它最多等待 30 秒接收其消息的回复。Actor 允许使用 org.codehaus.groovy.runtime.TimeCategory 类定义的时间 DSL 来指定 *react()* 方法的超时,前提是用户将调用包装在 *TimeCategory* 使用块中。

示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
def friend = Actors.actor {
    react {
        //this doesn't reply -> caller won't receive any answer in time
        println it
        //reply 'Hello' //uncomment this to answer conversation
        react {
            println it
        }
    }
}

def me = Actors.actor {
    friend.send('Hi')
    //wait for answer 1sec
    react(1000) {msg ->
        if (msg == Actor.TIMEOUT) {
            friend.send('I see, busy as usual. Never mind.')
            stop()
        } else {
            //continue conversation
            println "Thank you for $msg"
        }
    }
}

me.join()

当等待消息时超时过期时,会收到 Actor.TIMEOUT 消息。如果 Actor 上存在 *onTimeout()* 处理程序,也会调用该处理程序。

Actor 示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
def friend = Actors.actor {
    react {
        //this doesn't reply -> caller won't receive any answer in time
        println it
        //reply 'Hello' //uncomment this to answer conversation
        react {
            println it
        }
    }
}

def me = Actors.actor {
    friend.send('Hi')

    delegate.metaClass.onTimeout = {->
        friend.send('I see, busy as usual. Never mind.')
        stop()
    }

    //wait for answer 1sec
    react(1000) {msg ->
        if (msg != Actor.TIMEOUT) {
            //continue conversation
            println "Thank you for $msg"
        }
    }
}

me.join()

请注意,可以使用 **Groovy** 元编程来动态定义 Actor 的生命周期通知方法(例如 *onTimeout()*)。显然,当您决定为您的 Actor 定义一个新类时,可以以通常的方式定义生命周期方法。

示例
1
2
3
4
5
6
7
8
9
class MyActor extends DefaultActor {
    public void onTimeout() {
        ...
    }

    protected void act() {
       ...
    }
}

Actor 保证非线程安全代码的线程安全

Actor 保证一次最多只有一个线程处理 Actor 的主体。在幕后,每次将线程分配给 Actor 时都会同步内存。因此,Actor 的状态可以安全地由主体中的代码修改,而无需任何其他额外的(同步或锁定)工作。

示例
1
2
3
4
5
6
7
8
9
10
11
class MyCounterActor extends DefaultActor {
    private Integer counter = 0

    protected void act() {
        loop {
            react {
                counter++
            }
        }
    }
}

理想情况下,Actor 的代码永远不应该从外部直接调用,因此 Actor 类的所有代码只能由处理最后接收消息的线程执行。因此,所有 Actor 的代码都是 **隐式线程安全** 的。如果允许其他对象直接调用 Actor 的任何方法,则 Actor 代码和状态的线程安全保证将不再有效。

简单计算器

以下是一个更真实的基于事件的 Actor 示例,它接收两个数字消息,将它们加起来,并将结果发送到控制台 Actor。

一个计算器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import groovyx.gpars.group.DefaultPGroup

//not necessary, just showing that a single-threaded pool can still handle multiple actors
def group = new DefaultPGroup(1);

final def console = group.actor {
    loop {
        react {
            println 'Result: ' + it
        }
    }
}

final def calculator = group.actor {
    react {a ->
        react {b ->
            console.send(a + b)
        }
    }
}

calculator.send 2
calculator.send 3

calculator.join()
group.shutdown()

请注意,基于事件的 Actor 需要特别注意 *react()* 方法。由于 *基于事件的 Actor* 需要将代码分割成可按顺序分配给不同线程的独立块,并且 **延续** 在 JVM 上没有原生支持,因此这些块是人工创建的。*react()* 方法创建下一个消息处理程序。一旦当前消息处理程序完成,下一个消息处理程序(延续)就会被调度。


并发归并排序示例

为了比较,我还包括一个更复杂的示例,它使用 Actor 对整数列表进行并发归并排序。您可以看到,得益于 **Groovy** 的灵活性,我们非常接近 **Scala** 模型,尽管我仍然想念 **Scala** 中用于消息处理的模式匹配。

一个排序示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
import groovyx.gpars.group.DefaultPGroup
import static groovyx.gpars.actor.Actors.actor

Closure createMessageHandler(def parentActor) {
    return {
        react {List<Integer> message ->
            assert message != null
            switch (message.size()) {
                case 0..1:
                    parentActor.send(message)
                    break
                case 2:
                    if (message[0] <= message[1]) parentActor.send(message)
                    else parentActor.send(message[-1..0])
                    break
                default:
                    def splitList = split(message)

                    def child1 = actor(createMessageHandler(delegate))
                    def child2 = actor(createMessageHandler(delegate))
                    child1.send(splitList[0])
                    child2.send(splitList[1])

                    react {message1 ->
                        react {message2 ->
                            parentActor.send merge(message1, message2)
                        }
                    }
            }
        }
    }
}

def console = new DefaultPGroup(1).actor {
    react {
        println "Sorted array:\t${it}"
        System.exit 0
    }
}

def sorter = actor(createMessageHandler(console))
sorter.send([1, 5, 2, 4, 3, 8, 6, 7, 3, 9, 5, 3])
console.join()

def split(List<Integer> list) {
    int listSize = list.size()
    int middleIndex = listSize / 2
    def list1 = list[0..<middleIndex]
    def list2 = list[middleIndex..listSize - 1]
    return [list1, list2]
}

List<Integer> merge(List<Integer> a, List<Integer> b) {
    int i = 0, j = 0
    final int newSize = a.size() + b.size()
    List<Integer> result = new ArrayList<Integer>(newSize)

    while ((i < a.size()) && (j < b.size())) {
        if (a[i] <= b[j]) result << a[i++]
        else result << b[j++]
    }

    if (i < a.size()) result.addAll(a[i..-1])
    else result.addAll(b[j..-1])
    return result
}

由于 *Actor* 重用池中的线程,因此脚本将与几乎任何大小的线程池一起工作,无论沿途创建了多少个 Actor。


Actor 生命周期方法

每个 Actor 都可以定义生命周期观察方法,这些方法将在发生特定生命周期事件时被调用。

  • *afterStart()* - 在 Actor 启动后立即调用。

  • *afterStop(List undeliveredMessages)* - 在 Actor 停止后立即调用,传入队列中所有未处理的消息。

  • *onInterrupt(InterruptedException e)* - 当 Actor 的线程被中断时调用。线程中断将导致无论如何停止 Actor。

  • *onTimeout()* - 当在为当前阻塞 react 方法指定的超时时间内没有消息发送到 Actor 时调用。

  • *onException(Throwable e)* - 当 Actor 的事件处理程序中发生异常时调用。Actor 将在从该方法返回后停止。

您可以在您的 Actor 类中静态定义这些方法,也可以动态地将它们添加到 Actor 的元类中。

Actor 示例
1
2
3
4
5
6
7
8
9
10
11
12
class MyActor extends DefaultActor {
    public void afterStart() {
        ...
    }
    public void onTimeout() {
        ...
    }

    protected void act() {
       ...
    }
}
另一个示例
1
2
3
4
5
6
7
def myActor = actor {
    delegate.metaClass.onException = {
        log.error('Exception occurred', it)
    }

...
}
性能提示

为了提高性能,您可以在启动 *DynamicDispatchActor* 或 *ReactiveActor* 时考虑使用 *silentStart()* 方法而不是 *start()* 方法。调用 *silentStart()* 将绕过一些启动机制,因此也将避免调用 *afterStart()* 方法。由于其有状态的性质,*DefaultActor* 无法静默启动。

池管理

*Actor* 可以被组织成组,默认情况下,始终有一个应用程序范围的池化 Actor 组可用。并且,就像 *Actor* 抽象工厂一样,它可以用来在默认组中创建 Actor。自定义组可以用作抽象工厂来创建属于这些组的新 Actor 实例。

一个组示例
1
2
3
4
5
6
7
8
9
def myGroup = new DefaultPGroup()

def actor1 = myGroup.actor {
...
}

def actor2 = myGroup.actor {
...
}

Actor 的 *parallelGroup* 属性指向它所属的组。默认情况下,它指向默认 Actor 组,即 *Actors.defaultActorPGroup*,并且只能在 Actor 启动之前更改。

示例
1
2
3
4
5
6
7
8
class MyActor extends StaticDispatchActor<Integer> {
    private static PGroup group = new DefaultPGroup(100)

    MyActor(...) {
        this.parallelGroup = group
        ...
    }
}

属于同一组的 Actor 共享该组的底层线程池。默认情况下,池包含 n + 1 个线程,其中 **n** 代表 JVM 检测到的 **CPU** 数量。**池大小** 可以显式设置,可以通过设置 *gpars.poolsize* 系统属性,或者为每个 Actor 组单独设置。这是通过指定相应的构造函数参数来实现的。

示例
1
def myGroup = new DefaultPGroup(10)  //the pool will contain 10 threads

线程池可以通过相应的 *DefaultPGroup* 类进行操作,该类 **委托** 给线程池的 *Pool* 接口。例如,*resize()* 方法允许您随时更改池大小,而 *resetDefaultSize()* 方法将其恢复为默认值。当您需要安全地完成所有任务、销毁池并停止所有线程以有条理地退出 JVM 时,可以调用 *shutdown()* 方法。

示例
1
2
3
4
5
6
7
8
9
10
11
... (n+1 threads in the default pool after startup)

Actors.defaultActorPGroup.resize 1  //use one-thread pool

... (1 thread in the pool)

Actors.defaultActorPGroup.resetDefaultSize()

... (n+1 threads in the pool)

Actors.defaultActorPGroup.shutdown()

作为 *DefaultPGroup* 的替代方案,*DefaultPGroup* 创建了一个守护线程池,当需要非守护线程时,可以使用 *NonDaemonPGroup* 类。

示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def daemonGroup = new DefaultPGroup()

def actor1 = daemonGroup.actor {
...
}

def nonDaemonGroup = new NonDaemonPGroup()

def actor2 = nonDaemonGroup.actor {
...
}

class MyActor {
    def MyActor() {
        this.parallelGroup = nonDaemonGroup
    }

    void act() {...}
}

属于同一组的 Actor 共享底层线程池。使用 *池化 Actor 组*,您可以将您的 Actor 分割,以利用多个不同大小的线程池,从而将资源分配到系统的不同组件并调整其性能。

示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def coreActors = new NonDaemonPGroup(5)  //5 non-daemon threads pool
def helperActors = new DefaultPGroup(1)  //1 daemon thread pool

def priceCalculator = coreActors.actor {
...
}

def paymentProcessor = coreActors.actor {
...
}

def emailNotifier = helperActors.actor {
...
}

def cleanupActor = helperActors.actor {
...
}

//increase size of the core actor group
coreActors.resize 6

//shutdown the group's pool once you no longer need the group to release resources
helperActors.shutdown()

一旦您不再需要池化 Actor 组及其 Actor,请务必关闭自定义的池化 Actor 组,以保留系统资源。


默认 Actor 组

没有更改其 *parallelGroup* 属性或通过 *Actors* 类上的任何工厂方法创建的 Actor 可以共享一个公共组 *Actors.defaultActorPGroup*。该组使用一个 **可调整大小的线程池**,其上限为 **1000 个线程**。这为您提供了让池自动根据 Actor 的需求进行调整的便利。另一方面,随着 Actor 数量的增加,池可能会变得太大而效率低下。建议将您的 Actor 分组到您自己的 PGroup 中,使用固定大小的线程池,以便所有非微不足道的应用程序都能使用它。

常见陷阱:应用程序在 Actor 未收到消息时终止

您很可能正在使用守护线程和池,这是默认设置,而您的主线程已完成。在任何、部分或所有 Actor 上调用 *actor.join()* 将阻塞主线程,直到 Actor 终止,从而使所有 Actor 都保持运行状态。

或者,使用 *NonDaemonPGroup* 的实例并将一些 Actor 分配给这些组。

示例
1
2
def nonDaemonGroup = new NonDaemonPGroup()
def myActor = nonDaemonGroup.actor {...}

或者。一个示例

1
2
3
4
5
6
7
8
9
10
11
def nonDaemonGroup = new NonDaemonPGroup()

class MyActor extends DefaultActor {
    def MyActor() {
        this.parallelGroup = nonDaemonGroup
    }

    void act() {...}
}

def myActor = new MyActor()
阻塞 Actor

在某些情况下,您可能更喜欢使用阻塞 Actor,而不是基于事件的延续风格的 Actor。阻塞 Actor 在其整个生命周期内(包括等待消息的时间)都保留一个池化线程。它们避免了一些线程管理开销,因为它们在启动后永远不会争夺线程,并且还允许您编写直接的代码,而无需使用延续风格。由于它们只执行阻塞操作,因此消息读取是通过 *receive* 方法进行的。显然,并发运行的阻塞 Actor 的数量受共享池中可用线程数量的限制。另一方面,阻塞 Actor 通常比延续风格的 Actor 提供更好的性能,尤其是在 Actor 的消息队列很少为空时。

示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def decryptor = blockingActor {
    while (true) {
        receive {message ->
            if (message instanceof String) reply message.reverse()
            else stop()
        }
    }
}

def console = blockingActor {
    decryptor.send 'lellarap si yvoorG'
    println 'Decrypted message: ' + receive()
    decryptor.send false
}

[decryptor, console]*.join()

阻塞 Actor 增加了调整应用程序性能的选项数量。特别是,它们可能是您 Actor 网络中高流量位置的良好候选者。


无状态演员

动态调度 Actor

*DynamicDispatchActor* 类是一个 Actor,它允许使用消息处理代码的另一种结构。

总的来说,*DynamicDispatchActor* 会反复扫描消息,并将到达的消息调度到 Actor 上定义的 *onMessage(message)* 方法之一。*DynamicDispatchActor* 在幕后利用了 **Groovy** 的动态方法调度机制。由于与 *DefaultActor* 子类不同,*DynamicDispatchActor* 和 *ReactiveActor*(下面讨论)不需要在后续消息接收之间隐式记住 Actor 的状态,因此它们提供了更好的性能特征,通常与其他 Actor 框架(例如 Scala Actors)相当。

一个动态调度 Actor 示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import groovyx.gpars.actor.Actors
import groovyx.gpars.actor.DynamicDispatchActor

final class MyActor extends DynamicDispatchActor {

    void onMessage(String message) {
        println 'Received string'
    }

    void onMessage(Integer message) {
        println 'Received integer'
        reply 'Thanks!'
    }

    void onMessage(Object message) {
        println 'Received object'
        sender.send 'Thanks!'
    }

    void onMessage(List message) {
        println 'Received list'
        stop()
    }
}

final def myActor = new MyActor().start()

Actors.actor {
    myActor 1
    myActor ''
    myActor 1.0
    myActor(new ArrayList())
    myActor.join()
}.join()

在某些情况下,通常是在 Actor 不需要保留隐式对话历史相关的状态时,动态调度代码结构可能比使用嵌套的 *loop* 和 *react* 语句的传统代码结构更直观。

*DynamicDispatchActor* 类还提供了一个方便的功能,可以在 Actor 构造时或任何时候以后动态添加消息处理程序,使用 *when* 处理程序,可以选择将其包装在 *become* 方法中。

一个动态调度 Actor 示例
1
2
3
4
5
6
7
8
9
10
11
12
final Actor myActor = new DynamicDispatchActor().become {
    when {String msg -> println 'A String'; reply 'Thanks'}
    when {Double msg -> println 'A Double'; reply 'Thanks'}
    when {msg -> println 'A something ...'; reply 'What was that?';stop()}
}
myActor.start()
Actors.actor {
    myActor 'Hello'
    myActor 1.0d
    myActor 10 as BigDecimal
    myActor.join()
}.join()

显然,这两种方法可以结合使用。

一个组合示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
final class MyDDA extends DynamicDispatchActor {

    void onMessage(String message) {
        println 'Received string'
    }

    void onMessage(Integer message) {
        println 'Received integer'
    }

    void onMessage(Object message) {
        println 'Received object'
    }

    void onMessage(List message) {
        println 'Received list'
        stop()
    }
}

final def myActor = new MyDDA().become {
    when {BigDecimal num -> println 'Received BigDecimal'}
    when {Float num -> println 'Got a float'}
}.start()

Actors.actor {
    myActor 'Hello'
    myActor 1.0f
    myActor 10 as BigDecimal
    myActor.send([])
    myActor.join()
}.join()

通过when注册的动态消息处理程序将优先于静态onMessage处理程序。

动态调度Actor的公平或非公平行为

DynamicDispatchActor可以设置为以公平或非公平(默认)的方式运行。根据所选策略,Actor要么使线程可用于与同一并行组共享的其他Actor(公平),要么将线程保留给自己,直到消息队列为空(非公平)。

通常,非公平Actor的性能比公平Actor高2-3倍。

使用fairMessageHandler()工厂方法或Actor的makeFair()方法。

一个公平示例
1
    def fairActor = Actors.fairMessageHandler {...}

静态调度Actor

虽然DynamicDispatchActor根据消息的运行时类型调度消息,因此每次消息都会产生额外的性能损失,但StaticDispatchActor避免了运行时消息检查,仅根据编译时信息调度消息。

示例
1
2
3
4
5
6
7
8
9
10
11
12
13
final class MyActor extends StaticDispatchActor<String> {
    void onMessage(String message) {
        println 'Received string ' + message

        switch (message) {
            case 'hello':
                reply 'Hi!'
                break
            case 'stop':
                stop()
        }
    }
}

StaticDispatchActor的实例必须覆盖适合Actor声明类型参数的onMessage方法。然后使用每个接收到的消息调用onMessage(T message)方法。

通过辅助工厂方法,可以更快地获得公平或非公平的静态调度Actor。

示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
final actor = staticMessageHandler {String message ->
    println 'Received string ' + message

    switch (message) {
        case 'hello':
            reply 'Hi!'
            break
        case 'stop':
            stop()
    }
}

println 'Reply: ' + actor.sendAndWait('hello')
actor 'bye'
actor 'stop'
actor.join()

DynamicDispatchActor相比,StaticDispatchActor类仅限于单个处理程序方法。

这种简化的创建方式无需任何when处理程序,再加上相当大的性能优势,应使StaticDispatchActor成为您处理简单消息处理程序时的默认选择。当不需要根据消息运行时类型进行调度时,使用此方法。

例如,StaticDispatchActors使数据流操作符的运行速度比DynamicDispatchActor快四倍。


反应式Actor

ReactiveActor类通常通过调用Actors.reactor()DefaultPGroup.reactor()来构建,它允许采用更事件驱动的做法。

当反应式Actor接收到消息时,提供的代码块(构成反应式Actor的主体)将使用消息作为参数运行。从代码返回的结果作为回复发送。

示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
final def group = new DefaultPGroup()

final def doubler = group.reactor {
    2 * it
}

group.actor {
    println 'Double of 10 = ' + doubler.sendAndWait(10)
}

group.actor {
    println 'Double of 20 = ' + doubler.sendAndWait(20)
}

group.actor {
    println 'Double of 30 = ' + doubler.sendAndWait(30)
}

for(i in (1..10)) {
    println "Double of $i = ${doubler.sendAndWait(i)}"
}

doubler.stop()
doubler.join()

这是一个Actor的示例,该Actor将一批数字提交给ReactiveActor进行处理,然后随着结果的到来逐渐打印结果。

示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import groovyx.gpars.actor.Actor
import groovyx.gpars.actor.Actors

final def doubler = Actors.reactor {
    2 * it
}

Actor actor = Actors.actor {
    (1..10).each {doubler << it}
    int i = 0
    loop {
        i += 1
        if (i > 10) stop()
        else {
            react {message ->
                println "Double of $i = $message"
            }
        }
    }
}

actor.join()
doubler.stop()
doubler.join()

本质上,反应式Actor为一个Actor提供了一个方便的快捷方式,该Actor将在一个循环中等待消息,处理消息并发送回结果。这是反应式Actor在内部的示意图。

示例
1
2
3
4
5
6
7
8
9
10
11
public class ReactiveActor extends DefaultActor {
    Closure body

    void act() {
        loop {
            react {message ->
                reply body(message)
            }
        }
    }
}
反应式Actor的公平或非公平行为

ReactiveActor可以设置为以公平或非公平(默认)的方式运行。

根据所选策略,Actor要么使线程可用于与同一并行组共享的其他Actor(公平),要么将线程保留给自己,直到消息队列为空(非公平)。通常,非公平Actor的性能比公平Actor高2-3倍。

使用fairReactor()工厂方法或Actor的makeFair()方法。

一个公平示例
1
    def fairActor = Actors.fairReactor {...}

技巧和窍门

构建Actor代码

扩展DefaultActor类时,可以在act()方法中调用任何Actor的方法,并在其中使用react()loop()方法。

示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class MyDemoActor extends DefaultActor {

    protected void act() {
        handleA()
    }

    private void handleA() {
        react {a ->
            handleB(a)
        }
    }

    private void handleB(int a) {
        react {b ->
            println a + b
            reply a + b
        }
    }
}

final def demoActor = new MyDemoActor()
demoActor.start()

Actors.actor {
    demoActor 10
    demoActor 20
    react {
        println "Result: $it"
    }
}.join()

请记住,我们所有示例中的handleA()handleB()方法只会调度提供的消息处理程序,使其作为对下一条消息到达的反应的当前计算的延续运行。

或者,当使用actor()工厂方法时,可以通过元类添加事件处理代码,作为闭包。

示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
Actor demoActor = Actors.actor {
    delegate.metaClass {
        handleA = {->
            react {a ->
                 handleB(a)
            }
        }

        handleB = {a ->
            react {b ->
                println a + b
                reply a + b
            }
        }
    }

    handleA()
}

Actors.actor {
    demoActor 10
    demoActor 20
    react {
        println "Result: $it"
    }
}.join()

闭包(其委托设置为Actor)也可以用于构建事件处理代码。

示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
Closure handleB = {a ->
    react {b ->
        println a + b
        reply a + b
    }
}

Closure handleA = {->
    react {a ->
        handleB(a)
    }
}

Actor demoActor = Actors.actor {
    handleA.delegate = delegate
    handleB.delegate = delegate

    handleA()
}

Actors.actor {
    demoActor 10
    demoActor 20
    react {
        println "Result: $it"
    }
}.join()

事件驱动的循环

在编写事件驱动的Actor时,请记住,对react()loop()方法的调用具有略微不同的语义。

当您尝试在Actor中实现任何类型的循环时,这会成为一个挑战。另一方面,如果您利用react()仅调度一个延续并返回的事实,您可以在不担心堆栈溢出的情况下递归调用方法。请查看下面的示例,这些示例使用这三种描述的技术来构建Actor的代码。

DefaultActor的子类
示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class MyLoopActor extends DefaultActor {

    protected void act() {
        outerLoop()
    }

    private void outerLoop() {
        react {a ->
            println 'Outer: ' + a
            if (a != 0) innerLoop()
            else println 'Done'
        }
    }

    private void innerLoop() {
        react {b ->
            println 'Inner ' + b
            if (b == 0) outerLoop()
            else innerLoop()
        }
    }
}

final def actor = new MyLoopActor().start()
actor 10
actor 20
actor 0
actor 0
actor.join()

增强Actor的元类

示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
Actor actor = Actors.actor {

  delegate.metaClass {
      outerLoop = {->
          react {a ->
              println 'Outer: ' + a
              if (a!=0) innerLoop()
              else println 'Done'
          }
      }

      innerLoop = {->
          react {b ->
              println 'Inner ' + b
              if (b==0) outerLoop()
              else innerLoop()
          }
      }
  }

  outerLoop()
}

actor 10
actor 20
actor 0
actor 0
actor.join()

使用Groovy闭包

一个Groovy示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
Closure innerLoop

Closure outerLoop = {->
    react {a ->
        println 'Outer: ' + a
        if (a!=0) innerLoop()
        else println 'Done'
    }
}

innerLoop = {->
    react {b ->
        println 'Inner ' + b
        if (b==0) outerLoop()
        else innerLoop()
    }
}

Actor actor = Actors.actor {
    outerLoop.delegate = delegate
    innerLoop.delegate = delegate

    outerLoop()
}

actor 10
actor 20
actor 0
actor 0
actor.join()

另外,别忘了使用Actor的loop()方法创建直到Actor终止才运行的循环的想法。

示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class MyLoopingActor extends DefaultActor {

  protected void act() {
      loop {
          outerLoop()
      }
  }

  private void outerLoop() {
      react {a ->
          println 'Outer: ' + a
          if (a!=0) innerLoop()
          else println 'Done for now, but will loop again'
      }
  }

  private void innerLoop() {
      react {b ->
          println 'Inner ' + b
          if (b == 0) outerLoop()
          else innerLoop()
      }
  }
}

final def actor = new MyLoopingActor().start()
actor 10
actor 20
actor 0
actor 0
actor 10
actor.stop()
actor.join()

活动对象

活动对象在Actor之上提供了一个OO外观。这样,您就可以避免直接处理Actor机制,不必匹配消息,等待结果并发送回复。哎哟!

具有友好外观的Actor

示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import groovyx.gpars.activeobject.ActiveObject
import groovyx.gpars.activeobject.ActiveMethod

@ActiveObject
class Decryptor {
    @ActiveMethod
    def decrypt(String encryptedText) {
        return encryptedText.reverse()
    }

    @ActiveMethod
    def decrypt(Integer encryptedNumber) {
        return -1*encryptedNumber + 142
    }
}

final Decryptor decryptor = new Decryptor()
def part1 = decryptor.decrypt(' noitcA ni yvoorG')
def part2 = decryptor.decrypt(140)
def part3 = decryptor.decrypt('noitide dn')

print part1.get()
print part2.get()
println part3.get()

您使用@ActiveObject注解标记活动对象。这将确保为您的类的每个实例创建一个隐藏的Actor实例。现在,您可以使用@ActiveMethod注解标记方法,表示您希望该方法由目标对象的内部Actor异步调用。@ActiveMethod注解的可选布尔值blocking参数指定调用者是否应该阻塞,直到结果可用,或者调用者是否应该仅接收DataflowVariable形式的未来结果的promise,这样调用者就不会阻塞等待。

阻塞还是不阻塞?

默认情况下,所有活动方法都设置为非阻塞。但是,明确声明其返回类型的 method 必须配置为阻塞,否则编译器会报告错误。只有defvoidDataflowVariable是允许的非阻塞方法返回类型。

在幕后,GPars会将您的方法调用转换为发送到内部Actor的消息。该Actor最终将通过代表调用者调用所需的方法来处理该消息,并在完成后将回复发送回调用者。非阻塞方法返回结果的承诺,也称为DataflowVariables


但是阻塞意味着我们并不是真正异步的,对吗?

确实,如果您将活动方法标记为阻塞,则调用者将被阻塞等待结果,就像执行普通方法调用时一样。我们所实现的只是在活动对象中从并发访问中获得线程安全。synchronized关键字也可以为您提供这种功能。因此,应该是非阻塞方法驱动您决定使用活动对象。阻塞方法将提供通常的同步语义,但在并发方法调用中提供一致性保证。因此,当与非阻塞方法结合使用时,阻塞方法仍然非常有用。

示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import groovyx.gpars.activeobject.ActiveMethod
import groovyx.gpars.activeobject.ActiveObject
import groovyx.gpars.dataflow.DataflowVariable

@ActiveObject
class Decryptor {
    @ActiveMethod(blocking=true)
    String decrypt(String encryptedText) {
        encryptedText.reverse()
    }

    @ActiveMethod(blocking=true)
    Integer decrypt(Integer encryptedNumber) {
        -1*encryptedNumber + 142
    }
}

final Decryptor decryptor = new Decryptor()
print decryptor.decrypt(' noitcA ni yvoorG')
print decryptor.decrypt(140)
println decryptor.decrypt('noitide dn')
非阻塞语义

调用非阻塞活动方法将返回,一旦Actor被发送了一条消息。现在允许调用者执行任何它喜欢的事情,而Actor负责计算。

可以使用承诺上的bound属性轮询计算的状态。在返回的承诺上调用get()方法将阻塞调用者,直到值可用。对get()的调用最终将返回一个值或抛出一个异常,具体取决于实际计算的结果。


get()方法有一个带超时参数的变体,以避免无限等待的风险。

注解规则

在注释对象时,需要遵循一些规则。

  • ActiveMethod注解仅在注释为ActiveObject的类中被接受。

  • 只有实例(非静态)方法可以注释为ActiveMethod

  • 您可以用非活动方法覆盖活动方法,反之亦然。

  • 活动对象的子类可以声明额外的活动方法,前提是它们本身注释为ActiveObject

  • 组合使用活动方法和非活动方法可能会导致竞争条件。理想情况下,将活动对象设计为完全封装的类,其中所有非私有方法都标记为活动。

继承

@ActiveObject注解可以出现在继承层次结构中的任何类上。Actor字段只会在层次结构中最顶层注释的类中创建,子类将重用该字段。

一个带注解的示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import groovyx.gpars.activeobject.ActiveObject
import groovyx.gpars.activeobject.ActiveMethod
import groovyx.gpars.dataflow.DataflowVariable

@ActiveObject
class A {
    @ActiveMethod
    def fooA(value) {
        ...
    }
}

class B extends A {
}

@ActiveObject
class C extends B {
    @ActiveMethod
    def fooC(value1, value2) {
        ...
    }
}

在我们的示例中,Actor字段将生成到A类中。C类必须注释为@ActiveObject,因为它在fooC()方法上持有@ActiveMethod注解,而B类不需要注解,因为它的方法都没有是活动的。

就像Actor可以围绕线程池分组一样,活动对象可以配置为使用来自特定并行组的线程。

一个组示例
1
2
3
4
@ActiveObject("group1")
class MyActiveObject {
    ...
}

@ActiveObject注解的value参数指定将内部Actor绑定到的并行组的名称。只有来自指定组的线程将用于运行该类实例的内部Actor。

但是,组需要在创建属于该组的任何活动对象实例之前创建和注册。如果没有明确指定,活动对象将使用默认的Actor组 - Actors.defaultActorPGroup

示例
1
2
final DefaultPGroup group = new DefaultPGroup(10)
ActiveObjectRegistry.instance.register("group1", group)

内部Actor的替代名称

您可能很少遇到与活动对象的内部Actor字段的默认名称发生名称冲突的情况。如果您需要更改默认名称internalActiveObjectActor,请使用@ActiveObject注解的actorName参数。

一个命名示例
1
2
3
4
@ActiveObject(actorName = "alternativeActorName")
class MyActiveObject {
    ...
}
Actor命名约定

内部Actor的替代名称以及它们的目标组不能在子类中覆盖。

确保您只在继承层次结构中最顶层的活动对象中指定这些值。显然,最顶层的活动对象仍然允许子类化其他类,只是它的任何祖先都不能是活动对象。

经典示例

一些Actor使用示例

  • 埃拉托斯特尼筛法

  • 睡觉的理发师

  • 哲学家进餐

  • 单词排序

  • 负载均衡器


埃拉托斯特尼筛法

示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
import groovyx.gpars.actor.DynamicDispatchActor

/**
 * Demonstrates concurrent implementation of the Sieve of Eratosthenes using actors
 *
 * In principle, the algorithm consists of concurrently run chained filters,
 * each of which detects whether the current number can be divided by a single prime number.
 * (generate nums 1, 2, 3, 4, 5, ...) -> (filter by mod 2) -> (filter by mod 3) -> (filter by mod 5) -> (filter by mod 7) -> (filter by mod 11) -> (caution! Primes falling out here)
 * The chain is built (grows) on the fly, whenever a new prime is found.
 */

int requestedPrimeNumberBoundary = 1000

final def firstFilter = new FilterActor(2).start()

/**
 * Generating candidate numbers and sending them to the actor chain
 */
(2..requestedPrimeNumberBoundary).each {
    firstFilter it
}
firstFilter.sendAndWait 'Poison'

/**
 * Filter out numbers that can be divided by a single prime number
 */
final class FilterActor extends DynamicDispatchActor {
    private final int myPrime
    private def follower

    def FilterActor(final myPrime) { this.myPrime = myPrime; }

    /**
     * Try to divide the received number with the prime. If the number cannot be divided, send it along the chain.
     * If there's no-one to send it to, I'm the last in the chain, the number is a prime and so I will create and chain
     * a new actor responsible for filtering by this newly found prime number.
     */
    def onMessage(int value) {
        if (value % myPrime != 0) {
            if (follower) follower value
            else {
                println "Found $value"
                follower = new FilterActor(value).start()
            }
        }
    }

    /**
     * Stop the actor on poisson reception
     */
    def onMessage(def poisson) {
        if (follower) {
            def sender = sender
            follower.sendAndContinue(poisson, {this.stop(); sender?.send('Done')})  //Pass the poisson along and stop after a reply
        } else {  //I am the last in the chain
            stop()
            reply 'Done'
        }
    }
}

睡觉的理发师

示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
import groovyx.gpars.group.DefaultPGroup
import groovyx.gpars.actor.DefaultActor
import groovyx.gpars.group.DefaultPGroup
import groovyx.gpars.actor.Actor

final def group = new DefaultPGroup()

final def barber = group.actor {
    final def random = new Random()
    loop {
        react {message ->
            switch (message) {
                case Enter:
                    message.customer.send new Start()
                    println "Barber: Processing customer ${message.customer.name}"
                    doTheWork(random)
                    message.customer.send new Done()
                    reply new Next()
                    break
                case Wait:
                    println "Barber: No customers. Going to have a sleep"
                    break
            }
        }
    }
}

private def doTheWork(Random random) {
    Thread.sleep(random.nextInt(10) * 1000)
}

final Actor waitingRoom

waitingRoom = group.actor {
    final int capacity = 5
    final List<Customer> waitingCustomers = []
    boolean barberAsleep = true

    loop {
        react {message ->
            switch (message) {
                case Enter:
                    if (waitingCustomers.size() == capacity) {
                        reply new Full()
                    } else {
                        waitingCustomers << message.customer
                        if (barberAsleep) {
                            assert waitingCustomers.size() == 1
                            barberAsleep = false
                            waitingRoom.send new Next()
                        }
                        else reply new Wait()
                    }
                    break
                case Next:
                    if (waitingCustomers.size()>0) {
                        def customer = waitingCustomers.remove(0)
                        barber.send new Enter(customer:customer)
                    } else {
                        barber.send new Wait()
                        barberAsleep = true
                    }
            }
        }
    }

}

class Customer extends DefaultActor {
    String name
    Actor localBarbers

    void act() {
        localBarbers << new Enter(customer:this)
        loop {
            react {message ->
                switch (message) {
                    case Full:
                        println "Customer: $name: The waiting room is full. I am leaving."
                        stop()
                        break
                    case Wait:
                        println "Customer: $name: I will wait."
                        break
                    case Start:
                        println "Customer: $name: I am now being served."
                        break
                    case Done:
                        println "Customer: $name: I have been served."
                        stop();
                        break

                }
            }
        }
    }
}

class Enter { Customer customer }
class Full {}
class Wait {}
class Next {}
class Start {}
class Done {}

def customers = []
customers << new Customer(name:'Joe', localBarbers:waitingRoom).start()
customers << new Customer(name:'Dave', localBarbers:waitingRoom).start()
customers << new Customer(name:'Alice', localBarbers:waitingRoom).start()

sleep 15000
customers << new Customer(name: 'James', localBarbers: waitingRoom).start()
sleep 5000
customers*.join()
barber.stop()
waitingRoom.stop()

哲学家进餐

示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
import groovyx.gpars.actor.DefaultActor
import groovyx.gpars.actor.Actors

Actors.defaultActorPGroup.resize 5

final class Philosopher extends DefaultActor {
    private Random random = new Random()

    String name
    def forks = []

    void act() {
        assert 2 == forks.size()
        loop {
            think()
            forks*.send new Take()
            def messages = []
            react {a ->
                messages << [a, sender]
                react {b ->
                    messages << [b, sender]
                    if ([a, b].any {Rejected.isCase it}) {
                        println "$name: \tOops, can't get my forks! Giving up."
                        final def accepted = messages.find {Accepted.isCase it[0]}
                        if (accepted!=null) accepted[1].send new Finished()
                    } else {
                        eat()
                        reply new Finished()
                    }
                }
            }
        }
    }

    void think() {
        println "$name: \tI'm thinking"
        Thread.sleep random.nextInt(5000)
        println "$name: \tI'm done thinking"
    }

    void eat() {
        println "$name: \tI'm EATING"
        Thread.sleep random.nextInt(2000)
        println "$name: \tI'm done EATING"
    }
}

final class Fork extends DefaultActor {

    String name
    boolean available = true

    void act() {
        loop {
            react {message ->
                switch (message) {
                    case Take:
                        if (available) {
                            available = false
                            reply new Accepted()
                        } else reply new Rejected()
                        break
                    case Finished:
                        assert !available
                        available = true
                        break
                    default: throw new IllegalStateException("Cannot process the message: $message")
                }
            }
        }
    }
}

final class Take {}
final class Accepted {}
final class Rejected {}
final class Finished {}

def forks = [
        new Fork(name:'Fork 1'),
        new Fork(name:'Fork 2'),
        new Fork(name:'Fork 3'),
        new Fork(name:'Fork 4'),
        new Fork(name:'Fork 5')
]

def philosophers = [
        new Philosopher(name:'Joe', forks:[forks[0], forks[1]]),
        new Philosopher(name:'Dave', forks:[forks[1], forks[2]]),
        new Philosopher(name:'Alice', forks:[forks[2], forks[3]]),
        new Philosopher(name:'James', forks:[forks[3], forks[4]]),
        new Philosopher(name:'Phil', forks:[forks[4], forks[0]]),
]

forks*.start()
philosophers*.start()

sleep 10000
forks*.stop()
philosophers*.stop()

单词排序

给定一个文件夹名称,脚本将对文件夹中所有文件中的单词进行排序。SortMaster Actor创建给定数量的WordSortActors,将要对单词进行排序的文件分配给它们,并收集结果。

一个示例,排序
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
//Messages
private final class FileToSort { String fileName }
private final class SortResult { String fileName; List<String> words }

//Worker actor
class WordSortActor extends DefaultActor {

    private List<String> sortedWords(String fileName) {
        parseFile(fileName).sort {it.toLowerCase()}
    }

    private List<String> parseFile(String fileName) {
        List<String> words = []
        new File(fileName).splitEachLine(' ') {words.addAll(it)}
        return words
    }

    void act() {
        loop {
            react {message ->
                switch (message) {
                    case FileToSort:
                        println "Sorting file=${message.fileName} on thread ${Thread.currentThread().name}"
                        reply new SortResult(fileName: message.fileName, words: sortedWords(message.fileName))
                }
            }
        }
    }
}

//Master actor
final class SortMaster extends DefaultActor {

    String docRoot = '/'
    int numActors = 1

    List<List<String>> sorted = []
    private CountDownLatch startupLatch = new CountDownLatch(1)
    private CountDownLatch doneLatch

    private void beginSorting() {
        int cnt = sendTasksToWorkers()
        doneLatch = new CountDownLatch(cnt)
    }

    private List createWorkers() {
        return (1..numActors).collect {new WordSortActor().start()}
    }

    private int sendTasksToWorkers() {
        List<Actor> workers = createWorkers()
        int cnt = 0
        new File(docRoot).eachFile {
            workers[cnt % numActors] << new FileToSort(fileName: it)
            cnt += 1
        }
        return cnt
    }

    public void waitUntilDone() {
        startupLatch.await()
        doneLatch.await()
    }

    void act() {
        beginSorting()
        startupLatch.countDown()
        loop {
            react {
                switch (it) {
                    case SortResult:
                        sorted << it.words
                        doneLatch.countDown()
                        println "Received results for file=${it.fileName}"
                }
            }
        }
    }
}

//start the actors to sort words
def master = new SortMaster(docRoot: 'c:/tmp/Logs/', numActors: 5).start()
master.waitUntilDone()
println 'Done'

File file = new File("c:/tmp/Logs/sorted_words.txt")
file.withPrintWriter { printer ->
    master.sorted.each { printer.println it }
}

负载均衡器

演示了在可适应的工作人员集中进行工作平衡。负载均衡器接收任务并将它们排队到一个临时任务队列中。当一个工作人员完成他的任务时,它会向负载均衡器请求一个新任务。

如果负载均衡器在任务队列中没有可用任务,则工作人员将被停止。如果任务队列中的任务数量超过一定限制,则会创建一个新工作人员来增加工作人员池的大小。

一个负载均衡器示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
import groovyx.gpars.actor.Actor
import groovyx.gpars.actor.DefaultActor

/**
 * Demonstrates work balancing among adaptable set of workers.
 * The load balancer receives tasks and queues them in a temporary task queue.
 * When a worker finishes his assignment, it asks the load balancer for a new task.
 * If the load balancer doesn't have any tasks available in the task queue, the worker is stopped.
 * If the number of tasks in the task queue exceeds certain limit, a new worker is created
 * to increase size of the worker pool.
 */

final class LoadBalancer extends DefaultActor {
    int workers = 0
    List taskQueue = []
    private static final QUEUE_SIZE_TRIGGER = 10

    void act() {
        loop {
            react { message ->
                switch (message) {
                    case NeedMoreWork:
                        if (taskQueue.size() == 0) {
                            println 'No more tasks in the task queue. Terminating the worker.'
                            reply DemoWorker.EXIT
                            workers -= 1
                        } else reply taskQueue.remove(0)
                        break
                    case WorkToDo:
                        taskQueue << message
                        if ((workers == 0) || (taskQueue.size() >= QUEUE_SIZE_TRIGGER)) {
                            println 'Need more workers. Starting one.'
                            workers += 1
                            new DemoWorker(this).start()
                        }
                }
                println "Active workers=${workers}\tTasks in queue=${taskQueue.size()}"
            }
        }
    }
}

final class DemoWorker extends DefaultActor {
    final static Object EXIT = new Object()
    private static final Random random = new Random()

    Actor balancer

    def DemoWorker(balancer) {
        this.balancer = balancer
    }

    void act() {
        loop {
            this.balancer << new NeedMoreWork()
            react {
                switch (it) {
                    case WorkToDo:
                        processMessage(it)
                        break
                    case EXIT: terminate()
                }
            }
        }

    }

    private void processMessage(message) {
        synchronized (random) {
            Thread.sleep random.nextInt(5000)
        }
    }
}
final class WorkToDo {}
final class NeedMoreWork {}

final Actor balancer = new LoadBalancer().start()

//produce tasks
for (i in 1..20) {
    Thread.sleep 100
    balancer << new WorkToDo()
}

//produce tasks in a parallel thread
Thread.start {
    for (i in 1..10) {
        Thread.sleep 1000
        balancer << new WorkToDo()
    }
}

Thread.sleep 35000  //let the queues get empty
balancer << new WorkToDo()
balancer << new WorkToDo()
Thread.sleep 10000

balancer.stop()
balancer.join()

divider

用户指南:代理

Agent类是受Clojure中的Agent启发的线程安全非阻塞共享可变状态包装器实现。

无法避免共享可变状态

当您用体系结构消除对共享可变状态的需求时,许多并发问题都会消失。实际上,像ActorCSP数据流并发这样的概念完全避免或隔离了可变状态。

然而,在某些情况下,共享可变数据是不可避免的,或者使设计更自然易懂。例如,考虑典型的电子商务应用程序中的购物车,当多个AJAX请求可能并发地以读或写请求命中购物车时。

介绍

Clojure编程语言中,您可以找到Agent的概念,其目的是保护需要跨线程共享的可变数据。Agent隐藏数据并防止直接访问数据。客户端只能向Agent发送命令(函数)。这些命令将被序列化并依次针对数据处理。

由于命令是串行执行的,因此命令不需要关心并发,并在运行时可以假定数据都是它们自己的。尽管实现方式不同,但**GPars Agents**(称为 *Agent*)在本质上与**actors**的行为类似。它们接受消息并异步处理它们。但是,消息必须是命令(函数或**Groovy** 闭包),并且将在**agent**内部执行。接收后,接收到的函数将针对**Agent** 的内部状态运行,并且函数的返回值被认为是**Agent** 的新内部状态。

本质上,**agents** 通过只允许单个 *agent 管理的线程* 修改可变值来保护可变值。可变值**无法从外部直接访问**,而是 *需要向 agent 发送请求*,并且**agent** 保证按顺序代表调用者处理请求。**Agents** 保证所有请求的顺序执行,从而确保值的一致性。

示意 -
1
2
3
4
5
6
7
8
agent = new Agent(0)  //created a new Agent wrapping an integer with initial value 0
agent.send {increment()}  //asynchronous send operation, sending the increment() function
...

//after some delay to process the message the internal Agent's state has been updated
...

assert agent.val== 1

为了包装整数,我们当然可以使用**Java** 平台上的 `AtomicXXX` 类型,但是当状态是更复杂的对象时,我们需要更多支持。


概念

**GPars** 提供了一个**Agent** 类,它是一个受**Clojure** 中**Agents** 启发的特殊用途、线程安全、非阻塞实现。

一个**Agent** 包裹了一个对可变状态的引用,该引用保存在一个字段中,并接受代码(闭包或命令)作为消息,这些消息可以像发送给任何其他 actor 一样发送给**Agent**,使用 '<<' 运算符、`send()` 方法或 *隐式调用* 方法。

在接收闭包/命令后的某个时间点,闭包将针对内部可变字段调用,并可以对其进行更改。保证闭包在没有其他线程干预的情况下运行,因此可以自由地更改保存在内部 *data* 字段中的**Agent** 的内部状态。

整个更新过程属于 `fire-and-forget` 类型,因为一旦消息(闭包)发送到 Agent,调用者线程就可以去执行其他操作,并在稍后返回来使用**Agent.val** 或**Agent.valAsync(closure)** 检查当前值。

基本规则

  • 执行时,提交的命令获取**agent** 的状态作为参数。

  • 提交的命令/闭包可以调用 *agent* 状态上的任何方法。

  • 用新对象替换状态对象也是可以的,使用**updateValue()** 方法完成。

  • 提交的闭包的 *返回值* 没有特殊含义,会被忽略。

  • 如果发送给**Agent** 的消息 *不是闭包*,则它被视为内部引用字段的新值。

  • **Agent** 的 *val* 属性将等待 agent 队列中的所有先前命令都被使用,然后安全地返回**Agent** 的值。

  • *valAsync()* 方法将执行相同的操作,不会阻塞调用者。

  • *instantVal* 属性将返回内部**agent** 状态的即时快照。

  • 所有**Agent** 实例共享一个默认的守护线程池。设置**Agent** 实例的 *threadPool* 属性将允许它使用不同的线程池。

  • 命令抛出的异常可以使用 *errors* 属性收集。


示例

共享的成员列表

**Agent** 包裹了一个成员列表,这些成员已被添加到俱乐部。要添加新成员,必须向 *clubMembers* Agent 发送消息(添加成员的命令)。

示例 -
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import groovyx.gpars.agent.Agent import
java.util.concurrent.ExecutorService import java.util.concurrent.Executors

/**
 * Create a new Agent wrapping a list of strings
 */
def clubMembers = new Agent<List<String>>(['Me'])  //add Me

clubMembers.send {it.add 'James'}  //add James

final Thread t1 = Thread.start {
    clubMembers.send {it.add 'Joe'}  //add Joe
}

final Thread t2 = Thread.start {
    clubMembers << {it.add 'Dave'}  //add Dave
    clubMembers {it.add 'Alice'}    //add Alice (using the implicit call() method)
}

[t1, t2]*.join()
println clubMembers.val
clubMembers.valAsync {println "Current members: $it"}

clubMembers.await()

共享会议,计算注册人数

**Conference** 类允许注册和取消注册,但是这些方法只能从发送到 *conference* **Agent** 的命令中调用。

会议示例 -
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import groovyx.gpars.agent.Agent

/**
 * Conference stores number of registrations and allows parties to register and unregister.
 * It inherits from the Agent class and adds the register() and unregister() private methods,
 * which callers may use it the commands they submit to the Conference.
 */
class Conference extends Agent<Long> {
    def Conference() { super(0) }
    private def register(long num) { data += num }
    private def unregister(long num) { data -= num }
}

final Agent conference = new Conference()  //new Conference created

/**
 * Three external parties will try to register/unregister concurrently
 */

final Thread t1 = Thread.start {
    conference << {register(10L)}               //send a command to register 10 attendees
}

final Thread t2 = Thread.start {
    conference << {register(5L)}                //send a command to register 5 attendees
}

final Thread t3 = Thread.start {
    conference << {unregister(3L)}              //send a command to unregister 3 attendees
}

[t1, t2, t3]*.join()

assert 12L == conference.val

工厂方法

**Agent** 实例也可以使用 *Agent.agent()* 工厂方法创建。

创建 Agent 实例的示例
1
def clubMembers = Agent.agent ['Me']  //add Me

侦听器和验证器

Agents 允许用户添加监听器和验证器。监听器在内部状态每次更改时都会收到通知,而验证器则有机会通过抛出异常来拒绝或否决即将发生的更改。

具体示例 -
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
final Agent counter = new Agent()

counter.addListener {oldValue, newValue -> println "Changing value from $oldValue to $newValue"}
counter.addListener {agent, oldValue, newValue -> println "Agent $agent changing value from $oldValue to $newValue"}

counter.addValidator {oldValue, newValue -> if (oldValue > newValue) throw new IllegalArgumentException('Things can only go up in Groovy')}
counter.addValidator {agent, oldValue, newValue -> if (oldValue == newValue) throw new IllegalArgumentException('Things never stay the same for $agent')}

counter 10
counter 11
counter {updateValue 12}
counter 10  //Will be rejected

counter {updateValue it - 1}  //Will be rejected
counter {updateValue it}  //Will be rejected
counter {updateValue 11}  //Will be rejected
counter 12  //Will be rejected

counter 20
counter.await()

监听器和验证器本质上都是接受两个或三个参数的闭包。从验证器抛出的异常将在**agent** 内部记录,可以使用 *hasErrors()* 方法进行测试,或者通过 *errors* 属性检索。

错误测试示例
1
2
assert counter.hasErrors()
assert counter.errors.size() == 5

验证器注意事项

**Groovy** 对变量数据类型和不可变性并不严格,因此**agent** 用户应该注意潜在的障碍。

如果提交的代码直接修改状态,验证器将无法在验证规则违反的情况下撤销更改。有两种可能的解决方案

  • 确保你永远不会更改代表当前 agent 状态的提供的对象

  • 在 agent 上使用自定义复制策略,允许 agent 创建内部状态的副本

在这两种情况下,都需要调用 *updateValue()* 来设置和验证新状态。

问题以及这两个解决方案如下


验证器示例 -
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//Create an agent storing names, rejecting 'Joe'
final Closure rejectJoeValidator = {oldValue, newValue -> if ('Joe' in newValue) throw new IllegalArgumentException('Joe is not allowed to enter our list.')}

Agent agent = new Agent([])
agent.addValidator rejectJoeValidator

agent {it << 'Dave'}                    //Accepted
agent {it << 'Joe'}                     //Erroneously accepted, since by-passes the validation mechanism
println agent.val

//Solution 1 - never alter the supplied state object
agent = new Agent([])
agent.addValidator rejectJoeValidator

agent {updateValue(['Dave', * it])}      //Accepted
agent {updateValue(['Joe', * it])}       //Rejected
println agent.val

//Solution 2 - use custom copy strategy on the agent
agent = new Agent([], {it.clone()})
agent.addValidator rejectJoeValidator

agent {updateValue it << 'Dave'}        //Accepted
agent {updateValue it << 'Joe'}         //Rejected, since 'it' is now just a copy of the internal agent's state
println agent.val

分组

默认情况下,所有**Agent** 实例都属于同一个组,共享其守护线程池。

自定义组也可以创建**Agent** 实例。这些实例将属于创建它们的组,并将共享一个线程池。要创建属于组的**Agent** 实例,请在组上调用 *agent()* 工厂方法。这样你就可以组织和调整 agents 的性能。

围绕线程池创建组
1
2
final def group = new NonDaemonPGroup(5)  //create a group around a thread pool
def clubMembers = group.agent(['Me'])  //add Me
Agents 的自定义线程池

**agents** 的默认线程池包含守护线程。确保你的自定义线程池也使用守护线程,这可以通过使用**DefaultPGroup** 或向 *线程池构造函数* 提供自己的线程工厂来实现。

或者,如果你的线程池使用非守护线程,例如使用**NonDaemonPGroup** 组类时,确保你通过调用其 `shutdown()` 方法显式地关闭组或线程池,否则你的应用程序将 [red]永远不会退出。

直接池替换

或者,通过在**Agent** 实例上调用 *attachToThreadPool()* 方法,可以为其指定一个自定义线程池。

*attachToThreadPool()* 示例
1
2
3
4
def clubMembers = new Agent<List<String>>(['Me'])  //add Me

final ExecutorService pool = Executors.newFixedThreadPool(10)
clubMembers.attachToThreadPool(new DefaultPool(pool))
请记住,就像**actors** 一样,单个**Agent** 实例(又名 agent)一次只能使用一个线程

购物车示例

示例 -
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import groovyx.gpars.agent.Agent

class ShoppingCart {
    private def cartState = new Agent([:])
//----------------- public methods below here ----------------------------------
    public void addItem(String product, int quantity) {
        cartState << {it[product] = quantity}  //the << operator sends
                                               //a message to the Agent
    }    public void removeItem(String product) {
        cartState << {it.remove(product)}
    }    public Object listContent() {
        return cartState.val
    }    public void clearItems() {
        cartState << performClear
    }

    public void increaseQuantity(String product, int quantityChange) {
        cartState << this.&changeQuantity.curry(product, quantityChange)
    }
//----------------- private methods below here ---------------------------------
    private void changeQuantity(String product, int quantityChange, Map items) {
        items[product] = (items[product] ?: 0) + quantityChange
    }    private Closure performClear = { it.clear() }
}
//----------------- script code below here -------------------------------------
final ShoppingCart cart = new ShoppingCart()
cart.addItem 'Pilsner', 10
cart.addItem 'Budweisser', 5
cart.addItem 'Staropramen', 20

cart.removeItem 'Budweisser'
cart.addItem 'Budweisser', 15

println "Contents ${cart.listContent()}"

cart.increaseQuantity 'Budweisser', 3
println "Contents ${cart.listContent()}"

cart.clearItems()
println "Contents ${cart.listContent()}"

你可能已经注意到代码中的两种实现策略。

  1. 公共方法可能在内部只将所需的代码发送到**Agent**,而不是直接执行相同的函数。

因此,通常像这样的顺序代码
1
2
3
public void addItem(String product, int quantity) {
    cartState[product]=quantity
}
变成
1
2
3
public void addItem(String product, int quantity) {
    cartState << {it[product] = quantity}
}
  1. 公共方法可以发送对内部私有方法或闭包的引用,这些方法或闭包包含要执行的任务的所需功能。

公共到私有的示例
1
2
3
4
5
public void clearItems() {
    cartState << performClear
}

private Closure performClear = { it.clear() }

如果闭包除了当前内部状态实例之外还接受其他参数,**可能需要进行柯里化**。请参阅 *increaseQuantity* 方法。


打印机服务示例

另一个示例 - 假设一个非线程安全的打印机服务由多个线程共享。打印机需要在打印之前设置文档和质量属性。显然,如果没有适当的保护,我们就有可能发生竞争条件。调用者不希望在打印机可用之前阻塞,**actors** 的 `fire-and-forget` 特性非常优雅地解决了这个问题。

打印机服务示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import groovyx.gpars.agent.Agent

/**
 * A non-thread-safe service that slowly prints documents on at a time
 */
class PrinterService {
    String document
    String quality

    public void printDocument() {
        println "Printing $document in $quality quality"
        Thread.sleep 5000
        println "Done printing $document"
    }
}

def printer = new Agent<PrinterService>(new PrinterService())

final Thread thread1 = Thread.start {
    for (num in (1..3)) {
        final String text = "document $num"
        printer << {printerService ->
            printerService.document = text
            printerService.quality = 'High'
            printerService.printDocument()
        }
        Thread.sleep 200
    }
    println 'Thread 1 is ready to do something else. All print tasks have been submitted'
}

final Thread thread2 = Thread.start {
    for (num in (1..4)) {
        final String text = "picture $num"
        printer << {printerService ->
            printerService.document = text
            printerService.quality = 'Medium'
            printerService.printDocument()
        }
        Thread.sleep 500
    }
    println 'Thread 2 is ready to do something else. All print tasks have been submitted'
}

[thread1, thread2]*.join()
printer.await()

有关最新更新,请参阅相应的 演示

读取值

为了更紧密地遵循**Clojure** 的哲学,**Agent** 类对读取的优先级高于写入。通过使用 *instantVal* 属性,你的读取请求将绕过**Agent** 的传入消息队列,并返回内部状态的当前快照。*val* 属性将等待在消息队列中进行处理,就像非阻塞变体 *valAsync(Clojure cl)* 一样,它将使用内部状态作为参数调用提供的闭包。

你必须牢记,即使 *instantVal* 属性可能返回正确的结果,但它也可能返回随机的查看结果,因为 *instantVal* 执行时**Agent** 的内部状态是非确定性的,取决于在线程调度程序执行 *instantVal* 的主体之前已处理的消息。

*await()* 方法允许你等待在之前提交给**Agent** 的所有消息的处理,因此可能会阻塞调用线程。


状态复制策略

为了避免泄露内部状态,**Agent** 类可以将 `复制策略` 指定为第二个构造函数参数。指定 `复制策略` 后,内部状态将由 `复制策略` 闭包处理,并且 `复制策略` 值的输出值将返回给调用者,而不是实际的内部状态。这适用于 *instantVal*、*val* 以及 *valAsync()*。


错误处理

从提交的命令中抛出的异常将存储在**agent** 内部,并且可以从 *errors* 属性中获取。该属性一旦读取就会被清除。

错误处理示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def clubMembers = new Agent<List>()
assert clubMembers.errors.empty

    clubMembers.send {throw new IllegalStateException('test1')}
    clubMembers.send {throw new IllegalArgumentException('test2')}
    clubMembers.await()

    List errors = clubMembers.errors
    assert 2 == errors.size()
    assert errors[0] instanceof IllegalStateException
    assert 'test1' == errors[0].message
    assert errors[1] instanceof IllegalArgumentException
    assert 'test2' == errors[1].message

    assert clubMembers.errors.empty

公平与非公平 代理

**Agents** 可以是公平的也可以是非公平的。公平的**agents** 在处理完每条消息后就会放弃线程,非公平的**agents** 会一直保留线程,直到其消息队列为空。因此,非公平的**agents** 往往比公平的**agents** 性能更好。

所有**Agent** 实例的默认设置是**非公平的**,但是通过调用其 *makeFair()* 方法可以将实例设置为公平的。

使其公平的示例
1
2
def clubMembers = new Agent<List>(['Me'])  //add Me
clubMembers.makeFair()

divider

用户指南:数据流

**数据流** 并发提供了一种替代的并发模型,该模型本质上是安全且健壮的。


介绍

查看这个使用**GPars** 在**Groovy** 中编写的简短示例,该示例对三个并发运行的任务执行的计算结果进行求和

一个简单的示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import static groovyx.gpars.dataflow.Dataflow.task

final def x = new DataflowVariable()
final def y = new DataflowVariable()
final def z = new DataflowVariable()

task {
    z << x.val + y.val
}

task {
    x << 10
}

task {
    y << 5
}

println "Result: ${z.val}"

使用 *Dataflows* 类重写的相同算法如下所示

*Dataflows* 示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import static groovyx.gpars.dataflow.Dataflow.task

final def df = new Dataflows()

task {
    df.z = df.x + df.y
}

task {
    df.x = 10
}

task {
    df.y = 5
}

println "Result: ${df.z}"

我们启动三个逻辑任务,它们可以并行运行并执行各自的活动。这些任务需要交换数据,它们使用 `Dataflow Variables` 进行数据交换。将 `Dataflow Variables` 视为单次通道,可以安全可靠地将数据从生产者转移到消费者。

`Dataflow Variables` 的语义非常直观。当任务需要从 `DataflowVariable` 中读取一个值(通过 val 属性)时,它将阻塞,直到该值被另一个任务或线程设置(使用 '<<' 运算符)。每个 `Dataflow Variable` 在其生命周期中 **只能设置一次**。

请注意,您无需担心任务或线程的排序和同步,以及它们对共享变量的访问。这些值会在您无需干预的情况下,在适当的时间神奇地传递到各个任务之间。数据流在各个任务/线程之间无缝地传递,无需您的干预或关注。


实现细节

示例中的三个任务不一定需要映射到三个物理线程。任务代表所谓的“绿色”或“逻辑”线程,可以映射到任意数量的物理线程。实际映射取决于调度程序,但数据流算法的结果不取决于实际调度。

重新绑定是可能的

dataflow variablesbind操作会静默地接受重新绑定到一个值,该值等于已绑定的值。我们可以调用bindUnique方法以拒绝已绑定变量上的相等值。

优势

以下是使用Dataflow Concurrency(由Jonas Bonér提供)带来的好处

  • 无竞争条件

  • 无死锁

  • 确定性死锁

  • 完全确定性程序

  • 精美的代码。

这听起来还不错吧?


概念

数据流编程

引用维基百科

操作(在Dataflow程序中)由具有输入和输出的“黑盒”组成,所有这些都始终明确定义。它们一进入所有输入有效状态就会运行,而不是程序遇到它们时运行。传统程序本质上是一系列语句,如“现在执行此操作,然后执行此操作”,而dataflow程序更像是流水线上的一系列工人,他们将在物料到达后立即完成分配的任务。

这就是数据流语言本质上是并行的原因:操作没有需要跟踪的隐藏状态,并且所有操作都同时“准备就绪”。

原则

使用Dataflow Concurrency,您可以安全地跨任务共享变量。这些变量(在Groovy中的DataflowVariable类的实例中)只能被分配(使用'<<'运算符)一次值。另一方面,变量的值可以被多次读取(在Groovy中通过val属性读取),即使在分配值之前也可以。在这种情况下,读取任务会挂起,直到另一个任务设置值。因此,您可以简单地使用Dataflow Variables按顺序为每个任务编写代码,底层机制将确保您以线程安全的方式获取所有所需的值。

简而言之,您通常使用Dataflow variables执行三个操作

  • 创建dataflow variable

  • 等待变量绑定(读取它)

  • 绑定变量(写入它)

以下三个基本规则是您的程序必须遵循的

  • 当程序遇到未绑定变量时,它会等待值。

  • 绑定数据流变量的值后,无法再更改它。

  • Dataflow variables使得创建并发流代理变得容易。

数据流队列和广播

在查看Dataflow VariablesTasksOperators的示例之前,您应该了解一下流和队列,以便全面了解Dataflow Concurrency。除了dataflow variables之外,还有DataflowQueuesDataflowBroadcast的概念,您可以在代码中利用它们。

您可以将它们视为用于在并发任务或线程之间传递消息的线程安全缓冲区或队列。查看典型的生产者-消费者演示

生产者-消费者演示
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import static groovyx.gpars.dataflow.Dataflow.task

def words = ['Groovy', 'fantastic', 'concurrency', 'fun', 'enjoy', 'safe', 'GPars', 'data', 'flow']
final def buffer = new DataflowQueue()

task {
    for (word in words) {
        buffer << word.toUpperCase()  //add to the buffer
    }
}

task {
    while(true) println buffer.val  //read from the buffer in a loop
}

DataflowBroadcastsDataflowQueuesDataflowVariables一样,都实现了DataflowChannel接口,具有相同的用于写入它们和从它们读取值的公共方法。

通过DataflowChannel接口以相同方式处理这两种类型的能力,在您开始使用它们将tasksoperatorsselectors连接在一起时非常有用。

DataflowChannels 结合了两个接口

DataflowChannel接口结合了两个接口,每个接口都有其用途

  • DataflowReadChannel包含所有用于从通道读取值的必要方法 - getVal()getValAsync()whenBound()等。

  • DataflowWriteChannel包含所有用于将值写入通道的必要方法 - bind()'<<'

您可能更喜欢使用这些专用接口,而不是通用DataflowChannel接口,以更好地表达您的预期用法。

有关通道接口的更多详细信息,请参阅 API 文档。


点对点通信

DataflowQueue类可以看作是点对点(1 对 1,多对 1)通信通道。它允许一个或多个生产者向一个读取器发送消息。如果多个读取器从同一个DataflowQueue中读取,它们将各自消费不同的消息。

换句话说,每条消息只会被一个读取器消费。您可以轻松想象一个围绕共享DataflowQueue构建的简单负载均衡方案,当您的算法的消费者部分需要扩展时,读取器会动态添加。这也是连接任务或运算符时有用的默认选择。

发布-订阅通信

DataflowBroadcast类提供发布-订阅(1 对多,多对多)通信模型。一个或多个生产者写入消息,而所有已注册的读取器都会收到所有消息。因此,每条消息在写入通道时都会被所有具有有效订阅的读取器消费。读取器通过调用createReadChannel()方法订阅。

发布-订阅示例
1
2
3
4
5
6
7
8
9
10
11
DataflowWriteChannel broadcastStream = new DataflowBroadcast()
DataflowReadChannel stream1 = broadcastStream.createReadChannel()
DataflowReadChannel stream2 = broadcastStream.createReadChannel()

broadcastStream << 'Message1'
broadcastStream << 'Message2'
broadcastStream << 'Message3'

assert stream1.val == stream2.val
assert stream1.val == stream2.val
assert stream1.val == stream2.val

在幕后,DataflowBroadcast使用DataflowStream类来实现消息传递。


DataflowStream

DataflowStream类表示一个确定性数据流通道。它基于函数式队列的概念,因此为消息传递提供了无锁线程安全实现。

从本质上讲,您可以将DataflowStream机制视为 1 对多通信通道,因为当读取器消费一条消息时,其他读取器仍然可以读取同一条消息。此外,所有消息都按相同顺序到达所有读取器。

由于DataflowStream是作为函数式队列实现的,因此其 API 要求用户自己遍历流中的值。另一方面,DataflowStream提供了方便的值过滤或转换方法,以及有趣的性能特性。

DataflowStream的语义不同于DataflowChannel接口

与其他通信元素不同,DataflowStream类没有实现DataflowChannel接口,因为其使用语义不同。使用DataflowStreamReadAdapterDataflowStreamWriteAdapter类将DataflowChannel类的实例包装在DataflowReadChannelDataflowWriteChannel实现中。

DataflowStream 用法示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
import groovyx.gpars.dataflow.stream.DataflowStream
import groovyx.gpars.group.DefaultPGroup
import groovyx.gpars.scheduler.ResizeablePool

/**
 * Demonstrates concurrent implementation of the Sieve of Eratosthenes using dataflow tasks
 *
 * In principle, the algorithm consists of a concurrently run chained filters,
 * each of which detects whether the current number can be divided by a single prime number.
 * (generate nums 1, 2, 3, 4, 5, ...) -> (filter by mod 2) -> (filter by mod 3) -> (filter by mod 5) -> (filter by mod 7) -> (filter by mod 11) -> (caution! Primes falling out here)
 * The chain is built (grows) on the fly, whenever a new prime is found
 */

/**
 * We need a resizeable thread pool, since tasks consume threads while waiting, blocked for values from the DataflowQueue.val
 */
group = new DefaultPGroup(new ResizeablePool(true))

final int requestedPrimeNumberCount = 100

/**
 * Generating candidate numbers
 */
final DataflowStream candidates = new DataflowStream()
group.task {
    candidates.generate(2, {it + 1}, {it < 1000})
}

/**
 * Chain a new filter for a particular prime number to the end of the Sieve
 * @param inChannel The current end channel to consume
 * @param prime The prime number to divide future prime candidates with
 * @return A new channel ending the whole chain
 */
def filter(DataflowStream inChannel, int prime) {
    inChannel.filter { number ->
        group.task {
            number % prime != 0
        }
    }
}

/**
 * Consume Sieve output and add additional filters for all found primes
 */
def currentOutput = candidates
requestedPrimeNumberCount.times {

    int prime = currentOutput.first
    println "Found: $prime"
    currentOutput = filter(currentOutput, prime)
}

为了方便起见,以及为了能够将DataflowStream对象与其他数据流结构(例如运算符)一起使用,您可以使用DataflowReadAdapter包装它以进行读取访问,或使用DataflowWriteAdapter包装它以进行写入访问。

DataflowStream类专为单线程生产者和消费者设计。如果多个线程应该向流读取或写入值,则必须在外部串行化它们对流的访问,或者使用适配器。

DataflowStream 适配器

DataflowStream API及其使用语义与Dataflow(Read/Write)Channel定义的语义有很大不同。为了允许DataflowStreams与其他数据流元素一起工作,必须使用适配器。DataflowStreamReadAdapter类将用必要的读取值方法包装DataflowStream,而DataflowStreamWriteAdapter类将围绕包装的DataflowStream方法提供写入方法。

线程安全性

重要的是要注意,DataflowStreamWriteAdapter是线程安全的。它允许多个线程通过适配器向包装的DataflowStream添加值。另一方面,DataflowStreamReadAdapter旨在被单个线程使用。


DataflowStreamWriteAdapter是线程安全的

为了最大限度地减少开销并保持与DataflowStream语义的一致性,DataflowStreamReadAdapter类不是线程安全的,应仅在单个线程内使用。

如果多个线程需要从DataflowStream读取,它们应该创建自己的DataflowStreamReadAdapter包装。

由于有了适配器,DataflowStream可以用于运算符或选择器之间的通信,因为它们期望Dataflow(Read/Write)Channels

DataflowStreamAdapters 示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import groovyx.gpars.dataflow.DataflowQueue
import groovyx.gpars.dataflow.stream.DataflowStream
import groovyx.gpars.dataflow.stream.DataflowStreamReadAdapter
import groovyx.gpars.dataflow.stream.DataflowStreamWriteAdapter
import static groovyx.gpars.dataflow.Dataflow.selector
import static groovyx.gpars.dataflow.Dataflow.operator

/**
 * Demonstrates the use of DataflowStreamAdapters to allow dataflow operators to use DataflowStreams
 */

final DataflowStream a = new DataflowStream()
final DataflowStream b = new DataflowStream()
def aw = new DataflowStreamWriteAdapter(a)
def bw = new DataflowStreamWriteAdapter(b)
def ar = new DataflowStreamReadAdapter(a)
def br = new DataflowStreamReadAdapter(b)

def result = new DataflowQueue()

def op1 = operator(ar, bw) {
    bindOutput it
}
def op2 = selector([br], [result]) {
    result << it
}

aw << 1
aw << 2
aw << 3
assert([1, 2, 3] == [result.val, result.val, result.val])
op1.stop()
op2.stop()
op1.join()
op2.join()

此外,仅通过围绕DataflowStream的适配器才能使用从多个DataflowChannels中选择值的功能。

DataflowStream 示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import groovyx.gpars.dataflow.Select
import groovyx.gpars.dataflow.stream.DataflowStream
import groovyx.gpars.dataflow.stream.DataflowStreamReadAdapter
import groovyx.gpars.dataflow.stream.DataflowStreamWriteAdapter
import static groovyx.gpars.dataflow.Dataflow.select
import static groovyx.gpars.dataflow.Dataflow.task

/**
 * Demonstrates the use of DataflowStreamAdapters to allow dataflow select to select on DataflowStreams
 */

final DataflowStream a = new DataflowStream()
final DataflowStream b = new DataflowStream()

def aw = new DataflowStreamWriteAdapter(a)
def bw = new DataflowStreamWriteAdapter(b)
def ar = new DataflowStreamReadAdapter(a)
def br = new DataflowStreamReadAdapter(b)

final Select<?> select = select(ar, br)
task {
    aw << 1
    aw << 2
    aw << 3
}

assert 1 == select().value
assert 2 == select().value
assert 3 == select().value

task {
    bw << 4
    aw << 5
    bw << 6
}

def result = (1..3).collect{select()}.sort{it.value}

assert result*.value == [4, 5, 6]
assert result*.index == [1, 0, 1]

如果您不需要任何函数式队列DataflowStream-special功能(如生成、过滤或映射),可以考虑使用DataflowBroadcast类。

此类通过DataflowChannel接口提供发布-订阅通信模型。


绑定处理程序

什么是绑定
1
2
3
4
5
6
def a = new DataflowVariable()

a >> {println "The variable has just been bound to $it"}

a.whenBound {println "Just to confirm that the variable has been really set to $it"}
...

Bind handlers可以通过使用'>>'运算符和/或then()whenBound()方法,注册到所有数据流通道(变量、队列或广播)上。它们仅在将值绑定到变量后运行。

Dataflow queuesbroadcasts还支持wheneverBound方法,用于注册一个闭包或消息处理程序,以在每次将值绑定到它们时运行。

A DataflowQueue().wheneverBound 示例
1
2
def queue = new DataflowQueue()
queue.wheneverBound {println "A value $it arrived to the queue"}

显然,没有任何东西可以阻止您为一个承诺拥有多个处理程序:它们都将在承诺具有具体值后以并行方式触发

A wheneverBound 示例
1
2
3
4
5
6
7
8
Promise bookingPromise = task {
    final data = collectData()
    return broker.makeBooking(data)
}

bookingPromise.whenBound {booking -> printAgenda booking}
bookingPromise.whenBound {booking -> sendMeAnEmailTo booking}
bookingPromise.whenBound {booking -> updateTheCalendar booking}
并行推测?

数据流变量和广播是实现并行推测的几种可能方式之一。有关详细信息,请查看用户指南并行集合部分中的并行推测

绑定处理程序分组

当您需要等待多个DataflowVariables Promises绑定时,我们可以通过调用whenAllBound()函数来获益。它在Dataflow类和PGroup实例上都可用。

whenAllBound() 示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
    final group = new NonDaemonPGroup()

    //Calling asynchronous services and receiving back promises for the reservations
    Promise flightReservation = flightBookingService('PAR <-> BRU')
    Promise hotelReservation = hotelBookingService('BRU:Feb 24 20015 - Feb 29 2015')
    Promise taxiReservation = taxiBookingService('BRU:Feb 24 2015 10:31')

    //when all reservations have been made, we need to build an agenda for our trip
    Promise agenda = group.whenAllBound(flightReservation, hotelReservation, taxiReservation) {flight, hotel, taxi ->
        "Agenda: $flight | $hotel | $taxi"
    }

    //since this is a demo, we only print the agenda and block when it's ready
    println agenda.val

如果您不知道whenAllBound()处理程序需要的参数数量,则使用一个具有一个类型为List的参数的闭包

whenAllBound() 示例
1
2
3
4
5
6
7
8
9
10
11
Promise module1 = task {
    compile(module1Sources)
}
Promise module2 = task {
    compile(module2Sources)
}

//We don't know the number of modules that will be jarred together, so use a List
final jarCompiledModules = {List modules -> ...}

whenAllBound([module1, module2], jarCompiledModules)

绑定处理程序链式调用

所有数据流通道还支持then()方法,用于注册一个回调处理程序,以便在值可用时调用。与whenBound()不同,then()方法允许我们使用链式调用,使我们能够选择异步地在函数之间传递结果值。


Groovy允许我们在then()方法链中省略一些

一个无意义的示例 - 无需连接点!
1
2
3
4
5
6
final DataflowVariable variable = new DataflowVariable()
final DataflowVariable result = new DataflowVariable()

variable.then {it * 2} then {it + 1} then {result << it}
variable << 4
assert 9 == result.val
这可以很好地与异步函数
1
2
3
4
5
6
7
8
9
10
11
final DataflowVariable variable = new DataflowVariable()
final DataflowVariable result = new DataflowVariable()

final doubler = {it * 2}
final adder = {it + 1}

variable.then doubler then adder then {result << it}

Thread.start {variable << 4}

assert 9 == result.val
ActiveObjects
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@ActiveObject
class ActiveDemoCalculator {
    @ActiveMethod
    def doubler(int value) {
        value * 2
    }

    @ActiveMethod
    def adder(int value) {
        value + 1
    }
}

final DataflowVariable result = new DataflowVariable()
final calculator = new ActiveDemoCalculator();

calculator.doubler(4).then {calculator.adder it}.then {result << it}

assert 9 == result.val
链式承诺的动机

Chaining在从whenBound()处理程序中调用其他异步服务时可以节省大量代码。

异步服务(如异步函数活动方法)会返回其结果的Promises。为了获取实际结果,您的处理程序必须阻塞以等待值绑定。这会将当前线程锁定在非生产状态。

一个非生产性的示例
1
2
3
4
variable.whenBound {value ->
    Promise promise = asyncFunction(value)
    println promise.get()
}

或者,它可以注册另一个(嵌套的)whenBound()处理程序,这会导致代码变得不必要的复杂。

一个不必要的复杂嵌套示例
1
2
3
4
5
variable.whenBound {value ->
    asyncFunction(value).whenBound {
        println it
    }
}

为了说明,请比较以下两个代码片段。一个使用whenBound(),另一个使用then()链式调用。它们在功能和行为方面都等效。

A whenBound() 示例 + A then() 示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
final DataflowVariable variable = new DataflowVariable()

final doubler = {it * 2}
final inc = {it + 1}

//Using whenBound()
variable.whenBound {value ->
    task {
        doubler(value)
    }.whenBound {doubledValue ->
        task {
            inc(doubledValue)
        }.whenBound {incrementedValue ->
            println incrementedValue
        }
    }
}

//Using then() chaining
variable.then doubler then inc then this.&println

Thread.start {variable << 4}
链式承诺优雅地解决了这两个问题
1
variable >> asyncFunction >> {println it}

RightShift '>>' 运算符已被重载以调用 then() 方法,因此可以以相同的方式进行链接。

链接示例
1
2
3
4
5
6
7
8
9
10
11
final DataflowVariable variable = new DataflowVariable()
final DataflowVariable result = new DataflowVariable()

final doubler = {it * 2}
final adder = {it + 1}

variable >> doubler >> adder >> {result << it}

Thread.start {variable << 4}

assert 9 == result.val

Promise 链的错误处理

异步操作显然可能会抛出异常。能够轻松且毫不费力地处理它们非常重要。GPars Promise 对象可以隐式地将异步计算中的异常传播到 Promise 链中。

  • Promises 传播结果值以及异常。阻塞 get() 方法会重新抛出绑定到 Promise 的任何异常,以便调用者可以处理它。

  • 对于 异步通知 - whenBound() 处理程序闭包 - 将异常作为参数传递。

  • then() 方法接受两个参数 - 一个 值处理程序 和一个可选的 错误处理程序。这些将根据结果是常规值还是异常而被调用。如果没有指定 errorHandler,则异常将被重新抛出到 then() 返回的 Promise 中。

  • 对于 then() 方法完全相同的行为也适用于 whenAllBound() 方法,该方法监听多个 Promises 以进行绑定。

错误处理示例
1
2
3
4
5
6
7
8
9
10
Promise<Integer> initial = new DataflowVariable<Integer>()
Promise<String> result = initial.then {it * 2} then {100 / it}  // Will throw exception for 0
.then {println "Log the value $it as it passes by"; return it}  // No error handler is defined,
                                                                // so exceptions are ignored
                                                                // and silently re-thrown to the next handler in chain
.then({"The result for $num is $it"}, {"Error detected for $num: $it"}) // Here the exception is caught

initial << 0

println result.get()

ErrorHandler 是一个闭包,它接受 Throwable 的实例作为其唯一的(可选)参数。它返回一个应该绑定到 then() 方法调用结果的值,即返回的 Promise。如果在错误处理程序中抛出异常,则将其绑定到结果 Promise 作为错误。

重新抛出潜在异常
1
2
3
4
5
promise.then({it+1})                  // Implicitly re-throws potential exceptions bound to promise
promise.then({it+1}, {e -> throw e})  // Explicitly re-throws potential exceptions bound to promise

promise.then({it+1}, {e -> throw new RuntimeException('Error occurred', e})
// Explicitly re-throws a new exception wrapping a potential exception bound to a *Promise*

您希望将此异常放在哪里?

Java 中的异常处理有 try-catch 语句。GPars Promise 对象的行为使异步调用能够自由地以最方便的方式在任何地方处理异常。如果您愿意,您可以随意忽略代码中的异常,然后假设一切正常。即便如此,请记住异常不会意外被吞没。

异常示例
1
2
3
4
5
6
7
task {
    'gpars.org'.toURL().text  //should throw MalformedURLException
}

.then {page -> page.toUpperCase()}
.then {page -> page.contains('GROOVY')}
.then({mentionsGroovy -> println "Groovy found: $mentionsGroovy"}, {error -> println "Error: $error"}).join()

处理具体异常类型

您也可以像这样更具体地说明处理的异常类型

特定异常处理示例
1
2
3
4
5
url.then(download)
    .then(calculateHash, {MalformedURLException e -> return 0}) // <- specific !
    .then(formatResult)
    .then(printResult, printError)
    .then(sendNotificationEmail);
客户站点异常处理

您可能希望完全不处理异常,然后让客户端(使用者)处理它

延迟异常处理示例
1
2
3
4
5
6
Promise<Object> result = url.then(download).then(calculateHash).then(formatResult).then(printResult);
try {
    result.get()
} catch (Exception e) {
    //handle exceptions here
}

将所有内容整合在一起

通过结合 whenAllBound()then(或 '>>')方法,我们可以轻松地在方便的方式下管理大型异步场景。

大型异步示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
withPool {

    Closure download = {String url ->
        sleep 3000  //Simulate a web read
        'web content'
    }.asyncFun()

    Closure loadFile = {String fileName ->
        'file content'  //simulate a local file read
    }.asyncFun()

    Closure hash = {s -> s.hashCode()}

    Closure compare = {int first, int second ->
        first == second
    }

    Closure errorHandler = {println "Error detected: $it"}

    def all = whenAllBound([
                  download('http://www.gpars.org') >> hash,
                  loadFile('/coolStuff/gpars/website/index.html') >> hash
              ], compare).then({println it}, errorHandler)
    all.join()  //optionally block until the calculation is all done

请注意,只有初始操作(函数)需要是异步的。管道中更下游的函数将由您的 Promise 异步调用,即使它们是同步的。


使用 Promises 实现 Fork/join 模式

Promises 非常灵活,可以作为许多不同场景的实现工具。以下是 Promise 的一项额外的便捷功能。

_thenForkAndJoin() 方法在当前 Promise 绑定后触发一个或多个活动,并返回一个已完成的 Promise 对象,该对象仅在所有活动完成后绑定。

让我们看看这如何适应图片

  • then() - 允许活动链接,以便一个活动在另一个活动之后执行

  • whenAllBound() - 允许连接多个活动;只有在它们都完成后才会启动新的活动

  • task() - 允许我们创建(分叉)多个异步活动

  • thenForkAndJoin() - 用于分叉多个活动并在它们上进行连接的简写语法

因此,使用 thenForkAndJoin(),您只需创建多个应由共享(触发)Promise 触发的活动。

多个活动示例
1
promise.thenForkAndJoin(task1, task2, task3).then{...}

一旦所有活动返回结果,它们就会被收集到一个列表中,并绑定到 thenForkAndJoin() 返回的 Promise 中。

thenForkAndJoin() 示例
1
2
3
task {
    2
}.thenForkAndJoin({ it ** 2 }, { it**3 }, { it**4 }, { it**5 }).then({ println it}).join()

延迟 数据流 任务和变量

有时您可能需要将 数据流变量 的特性与延迟初始化相结合。

延迟示例
1
2
3
4
5
6
Closure<String> download = {url ->
    println "Downloading"
    url.toURL().text
}

def pageContent = new LazyDataflowVariable(download.curry("https://gpars.java.net.cn"))

LazyDataflowVariable 的实例在构造时声明了初始化程序。仅当有人通过阻塞 get() 方法或使用任何非阻塞回调方法(如 then())请求其值时才会触发实例。由于 LazyDataflowVariables 保留了普通 DataflowVariables 的所有优点,因此您可以将它们与其他 延迟普通 数据流变量 轻松链接在一起。


更大的示例

本讨论值得一个更实际的示例。因此,从 这篇长文 中汲取灵感,以下代码片段演示了如何使用 LazyDataflowVariables 将相互依赖的组件延迟且异步地加载到内存中。组件模块将按照其依赖项的顺序加载,如果可能,将并发加载。

每个模块将只加载一次,无论有多少模块依赖于它。由于 延迟,只有那些被传递依赖的模块才会被加载。我们的示例使用简单的“菱形”依赖关系方案

  • D 依赖于 B 和 C

  • C 依赖于 A

  • B 依赖于 A

加载 D 时,A 将首先被加载。B 和 C 将在 A 加载完成后并发加载。D 将在 B 和 C 都加载完成后开始加载。

菱形示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
def moduleA = new LazyDataflowVariable({->
    println "Loading moduleA into memory"
    sleep 3000
    println "Loaded moduleA into memory"
    return "moduleA"
})

def moduleB = new LazyDataflowVariable({->
    moduleA.then {
        println "->Loading moduleB into memory, since moduleA is ready"
        sleep 3000
        println "  Loaded moduleB into memory"
        return "moduleB"
    }
})

def moduleC = new LazyDataflowVariable({->
    moduleA.then {
        println "->Loading moduleC into memory, since moduleA is ready"
        sleep 3000
        println "  Loaded moduleC into memory"
        return "moduleC"
    }
})

def moduleD = new LazyDataflowVariable({->
    whenAllBound(moduleB, moduleC) { b, c ->
        println "-->Loading moduleD into memory, since moduleB and moduleC are ready"
        sleep 3000
        println "   Loaded moduleD into memory"
        return "moduleD"
    }
})

println "Nothing loaded so far"
println "==================================================================="
println "Load module: " + moduleD.get()
println "==================================================================="
println "All requested modules loaded"

使任务延迟

lazyTask() 方法与 task() 方法一起提供,为我们提供了用于延迟活动的以任务为中心的抽象。延迟任务 返回一个 LazyDataflowVariable 的实例(就像 Promise 一样),其初始化程序由提供的闭包设置。一旦有人请求该值,该任务将异步启动,并最终将一个值传递到 LazyDataflowVariable 中。

延迟示例
1
2
3
4
5
6
7
8
9
10
11
12
13
import groovyx.gpars.dataflow.Dataflow

def pageContent = Dataflow.lazyTask {
        println "Downloading"
        "https://gpars.java.net.cn".toURL().text
    }

println "No-one has asked for the value just yet. Bound = ${pageContent.bound}"
sleep 1000
println "Now going to ask for a value"
println pageContent.get().size()
println "Repetitive requests will receive the already calculated value. No additional downloading."
println pageContent.get().size()

数据流表达式

看看下面的魔法

数据流示例
1
2
3
4
5
6
7
8
9
10
11
12
def initialDistance = new DataflowVariable()
def acceleration = new DataflowVariable()
def time = new DataflowVariable()

task {
    initialDistance << 100
    acceleration << 2
    time << 10
}

def result = initialDistance + acceleration*0.5*time**2
println 'Total distance ' + result.val

我们使用 DataflowVariables 来表示加速度物体的总距离的数学方程的几个参数。然而,在方程本身中,我们直接使用 DataflowVariable。我们不引用它们表示的值,但我们仍然能够正确地进行数学运算。这表明 DataflowVariables 可以非常灵活。

例如,您可以调用它们的方法,这些方法会分派到绑定的值

DataflowVariable 示例
1
2
3
4
5
def name = new DataflowVariable()
task {
    name << '  adam   '
}
println name.toUpperCase().trim().val

您可以将其他 DataflowVariables 作为参数传递给这些方法,并且真实的值将自动传递代替

另一个 DataflowVariable 作为参数示例
1
2
3
4
5
6
7
8
9
10
11
def title = new DataflowVariable()
def searchPhrase = new DataflowVariable()
task {
    title << ' Groovy in Action 2nd edition   '
}

task {
    searchPhrase << '2nd'
}

println title.trim().contains(searchPhrase).val

您也可以使用 DataflowVariable 直接查询绑定值的属性

用于查询图书标题属性的 DataflowVariable 示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def book = new DataflowVariable()
def searchPhrase = new DataflowVariable()
task {
    book << [
             title:'Groovy in Action 2nd edition   ',
             author:'Dierk Koenig',
             publisher:'Manning']
}

task {
    searchPhrase << '2nd'
}

book.title.trim().contains(searchPhrase).whenBound {println it}  //Asynchronous waiting

println book.title.trim().contains(searchPhrase).val  //Synchronous waiting

请注意,结果仍然是 DataflowVariable(准确地说是 DataflowExpression),您可以从中同步和异步地获取真实值。


绑定错误通知

DataflowVariables 提供了在每次绑定操作失败时向注册的侦听器发送通知的功能。getBindErrorManager() 方法允许添加和删除侦听器。在尝试绑定值(通过 bind()bindSafely()bindUnique()leftShift())失败或发生错误(通过 bindError())时,会通知侦听器。

报告绑定操作失败
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
        final DataflowVariable variable = new DataflowVariable()

        variable.getBindErrorManager().addBindErrorListener(new BindErrorListener() {
            @Override
            void onBindError(final Object oldValue, final Object failedValue, final boolean uniqueBind) {
                println "Bind failed!"
            }

            @Override
            void onBindError(final Object oldValue, final Throwable failedError) {
                println "Binding an error failed!"
            }

            @Override
            public void onBindError(final Throwable oldError, final Object failedValue, final boolean uniqueBind) {
                println "Bind failed!"
            }

            @Override
            public void onBindError(final Throwable oldError, final Throwable failedError) {
                println "Binding an error failed!"
            }

        })

这使我们能够自定义对任何尝试绑定已绑定 Dataflow Variable 的响应。例如,使用 bindSafely(),您不会将绑定异常传回给调用者,而是会通知注册的 BindErrorListener

任务

数据流任务 为我们提供了一个易于理解的相互独立的逻辑任务或线程的抽象。这些可以并发运行,并仅通过 数据流变量队列广播 交换数据。数据流任务 以及它易于表达的相互依赖关系和本质上顺序的体,也可以用作 UML 活动图 的实用实现。

查看示例。


一个简单的混搭示例

在这个示例中,我们正在每个任务中下载三个热门网站的首页,同时在另一个任务中,我们正在过滤掉今天谈论 Groovy 的网站并形成输出。输出任务通过三个数据流变量自动与三个下载任务同步,通过这些变量,每个网站的内容被传递到输出任务。

多么奇妙的混搭!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import static groovyx.gpars.GParsPool.withPool
import groovyx.gpars.dataflow.DataflowVariable
import static groovyx.gpars.dataflow.Dataflow.task


/**
 * A simple mashup sample, downloads content of three websites
 * and checks how many of them refer to Groovy.
 */

def dzone = new DataflowVariable()
def jroller = new DataflowVariable()
def theserverside = new DataflowVariable()

task {
    println 'Started downloading from DZone'
    dzone << 'http://www.dzone.com'.toURL().text
    println 'Done downloading from DZone'
}

task {
    println 'Started downloading from JRoller'
    jroller << 'http://www.jroller.com'.toURL().text
    println 'Done downloading from JRoller'
}

task {
    println 'Started downloading from TheServerSide'
    theserverside << 'http://www.theserverside.com'.toURL().text
    println 'Done downloading from TheServerSide'
}

task {
    withPool {
        println "Number of Groovy sites today: " +
                ([dzone, jroller, theserverside].findAllParallel {
                    it.val.toUpperCase().contains 'GROOVY'
                }).size()
    }
}.join()

分组任务

数据流任务可以组织成组,以便进行性能微调。组提供了一个方便的 task() 工厂方法来创建附加到这些组的任务。使用组允许我们围绕不同的线程池(封装在组中)组织任务或运算符。虽然 Dataflow.task() 命令将任务调度到默认线程池 (java.util.concurrent.Executor, 固定大小 = #cpu+1, 守护线程) 上,但我们可能更愿意定义我们自己的线程池来运行这些任务。

个人线程池示例
1
2
3
4
5
6
7
8
9
10
11
12
13
import groovyx.gpars.group.DefaultPGroup

def group = new DefaultPGroup()

group.with {
    task {
        ...
    }

    task {
        ...
    }
}
数据流 的自定义线程池

数据流任务的默认线程池具有守护线程。这意味着我们的应用程序将在主线程完成时立即退出,并且 不会 等待所有任务完成!

分组任务时,请确保自定义线程池

  1. 使用守护线程(通过使用 DefaultPGroup 实现)

  2. 向线程池构造函数提供线程工厂

  3. 或者,如果线程池使用非守护线程(来自 NonDaemonPGroup 组类),我们必须通过调用其 shutdown() 方法显式地关闭该组或线程池,否则我们的应用程序将不会退出。

我们可以在代码块中使用 Dataflow.usingGroup() 方法选择性地覆盖用于任务、运算符、回调和其他数据流元素的默认组。

示例
1
2
3
4
5
6
7
8
Dataflow.usingGroup(group) {
    task {
        'http://gpars.codehaus.org'.toURL().text  //should throw MalformedURLException
    }
    .then {page -> page.toUpperCase()}
    .then {page -> page.contains('GROOVY')}
    .then({mentionsGroovy -> println "Groovy found: $mentionsGroovy"}, {error -> println "Error: $error"}).join()
}

您可以始终通过具体化来覆盖默认组

示例
1
2
3
4
5
6
7
8
Dataflow.usingGroup(group) {
    anotherGroup.task {
        'http://gpars.codehaus.org'.toURL().text  //should throw MalformedURLException
    }
    .then(anotherGroup) {page -> page.toUpperCase()}
    .then(anotherGroup) {page -> page.contains('GROOVY')}.then(anotherGroup) {println Dataflow.retrieveCurrentDFPGroup();it}
    .then(anotherGroup, {mentionsGroovy -> println "Groovy found: $mentionsGroovy"}, {error -> println "Error: $error"}).join()
}

带有方法的混搭变体

为了避免给您造成关于构建数据流代码的错误印象,这里是对混搭示例的重写,其中 downloadPage() 方法在单独的任务中执行实际的下载。它返回一个 DataflowVariable 实例,以便主应用程序线程最终能够获取下载的内容。

数据流变量显然可以作为参数或返回值传递。

示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package groovyx.gpars.samples.dataflow

import static groovyx.gpars.GParsExecutorsPool.withPool
import groovyx.gpars.dataflow.DataflowVariable
import static groovyx.gpars.dataflow.Dataflow.task


/**
 * A simple mashup sample, downloads content of three websites and checks how many of them refer to Groovy.
 */
final List urls = ['http://www.dzone.com', 'http://www.jroller.com', 'http://www.theserverside.com']

task {
    def pages = urls.collect { downloadPage(it) }
    withPool {
        println "Number of Groovy sites today: " +
                (pages.findAllParallel {
                    it.val.toUpperCase().contains 'GROOVY'
                }).size()
    }
}.join()

def downloadPage(def url) {
    def page = new DataflowVariable()
    task {
        println "Started downloading from $url"
        page << url.toURL().text
        println "Done downloading from $url"
    }
    return page
}

物理计算示例

数据流程序自然会随着处理器数量的增加而扩展。在一定程度上,您拥有的处理器越多,程序运行速度就越快。例如,查看以下脚本,该脚本计算一个简单的物理实验的参数并打印出结果。

每个任务执行其计算的一部分,可能依赖于其他任务计算的值,并且其结果可能需要其他任务使用。使用 数据流并发,您可以根据需要将工作在任务之间拆分或重新排序任务本身,数据流机制将确保计算正确完成。

DataflowVariable 示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
import groovyx.gpars.dataflow.DataflowVariable
import static groovyx.gpars.dataflow.Dataflow.task

final def mass = new DataflowVariable()
final def radius = new DataflowVariable()
final def volume = new DataflowVariable()
final def density = new DataflowVariable()
final def acceleration = new DataflowVariable()
final def time = new DataflowVariable()
final def velocity = new DataflowVariable()
final def decelerationForce = new DataflowVariable()
final def deceleration = new DataflowVariable()
final def distance = new DataflowVariable()

def t = task {
    println """

Calculating distance required to stop a moving ball.
....................................................
The ball has a radius of ${radius.val} meters and is made of a material with ${density.val} kg/m3 density,
which means that the ball has a volume of ${volume.val} m3 and a mass of ${mass.val} kg.
The ball has been accelerating with ${acceleration.val} m/s2 from 0 for ${time.val} seconds and so reached a velocity of ${velocity.val} m/s.

Given our ability to push the ball backwards with a force of ${decelerationForce.val} N (Newton), we can cause a deceleration
of ${deceleration.val} m/s2 and so stop the ball at a distance of ${distance.val} m.
...................................................

This example has been calculated asynchronously in multiple tasks using *GPars* Dataflow concurrency in Groovy.
Author: ${author.val}
"""

    System.exit 0
}

task {
    mass << volume.val * density.val
}

task {
    volume << Math.PI * (radius.val ** 3)
}

task {
    radius << 2.5
    density <<         998.2071  //water
    acceleration << 9.80665 //free fall
    decelerationForce << 900
}

task {
    println 'Enter your name:'
    def name = new InputStreamReader(System.in).readLine()
    author << (name?.trim()?.size()>0 ? name : 'anonymous')
}

task {
    time << 10
    velocity << acceleration.val * time.val
}

task {
    deceleration << decelerationForce.val / mass.val
}

task {
    distance << deceleration.val * ((velocity.val/deceleration.val) ** 2) * 0.5
}

t.join()

我尽力使所有物理计算都正确。随意更改值,看看你需要多远才能停止滚动球。

确定性死锁

如果您碰巧在依赖关系中引入了死锁,则每次运行代码时都会发生死锁。不允许随机性。这是 数据流并发 的优势之一。无论实际的线程调度方案如何,如果您在测试中没有出现死锁,那么您在生产中也不会出现死锁。

确定性死锁示例
1
2
3
4
5
6
7
8
9
task {
    println a.val
    b << 'Hi there'
}

task {
    println b.val
    a << 'Hello man'
}

数据流 映射

作为一种便捷的快捷方式,Dataflows 类可以帮助您减少需要使用 Dataflow Variables 的代码量。

一个方便的示例
1
2
3
4
5
6
7
8
def df = new Dataflows()
df.x = 'value1'

assert df.x == 'value1'

Dataflow.task {df.y = 'value2}

assert df.y == 'value2'

Dataflows 想象成一个地图,其中 Dataflow Variables 是键,存储与其绑定的值作为相应的地图值。读取值(例如 df.x)和绑定值(例如 df.x = 'value')的语义与普通 Dataflow Variables(分别为 x.val 和 x << 'value')的语义相同。


混合 DataflowsGroovy with

Dataflows 实例的 with 块内,存储在 Dataflows 实例中的 Dataflow Variables 可以直接访问,无需在其前面加上 Dataflows 实例标识符。

with 块使编码更轻松
1
2
3
4
5
6
7
8
new Dataflows().with {
    x = 'value1'
    assert x == 'value1'

    Dataflow.task {y = 'value2}

    assert y == 'value2'
}

从任务中返回值

通常,Dataflow 任务通过 Dataflow Variables 进行通信。除此之外,任务还可以返回值,同样通过 Dataflow Variable。当您调用 task() 工厂方法时,您会获得一个 Promise 实例(实现为 DataflowVariable),您可以通过它监听任务的返回值,就像使用任何其他 PromiseDataflowVariable 一样。

任务使用 Promise 返回值
1
2
3
4
5
6
7
8
9
10
    final Promise t1 = task {
        return 10
    }
    final Promise t2 = task {
        return 20
    }

    def results = [t1, t2]*.val

    println 'Both sub-tasks finished and returned values: ' + results

也可以在不阻塞调用者的前提下使用 whenBound() 方法获取值。

示例
1
2
3
4
5
def task = task {
    println 'The task is running and calculating the return value'
    30        // the value to be returned
}
task >> {value -> println "The task finished and returned $value"}

加入任务

在任务生成的 Dataflow Variable 上使用 join() 操作,您可以阻塞直到任务完成。

一个阻塞示例
1
2
3
4
5
6
7
8
9
10
 task {
     final Promise t1 = task {
         println 'First sub-task running.'
     }
     final Promise t2 = task {
         println 'Second sub-task running'
     }
     [t1, t2]*.join()
     println 'Both sub-tasks finished'
 }.join()

选择

通常,需要从多个数据流通道(如变量、队列、广播或流)之一获取值。Select 类适用于这些场景。

Select 可以扫描多个数据流通道,并从所有输入通道中选择一个通道,该通道包含一个准备读取的值。从该选定通道中读取的值将与源通道的索引一起返回给调用者。选择通道是随机的,或者基于通道优先级,在这种情况下,在 Select 构造函数中具有较低位置索引的通道具有较高优先级。

从多个通道中选择值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import groovyx.gpars.dataflow.DataflowQueue
import groovyx.gpars.dataflow.DataflowVariable
import static groovyx.gpars.dataflow.Dataflow.select
import static groovyx.gpars.dataflow.Dataflow.task

/**
 * Shows a basic use of Select, which monitors a set of input channels for values and makes these values
 * available on its output irrespective of their original input channel.
 * Note that dataflow variables and queues can be combined for Select.
 *
 * You might also consider checking out the prioritySelect method, which prioritizes values by the index of their input channel
 */
def a = new DataflowVariable()
def b = new DataflowVariable()
def c = new DataflowQueue()

task {
    sleep 3000
    a << 10
}

task {
    sleep 1000
    b << 20
}

task {
    sleep 5000
    c << 30
}

def select = select([a, b, c])

println "The fastest result is ${select().value}"
select() 方法返回什么?

请注意,select() 的返回值类型是 SelectResult,它包含值和原始通道索引。

有多种方法可以从 Select 中读取值

我如何选择你?让我数一数方法!
1
2
3
4
5
6
7
8
def sel = select(a, b, c, d)
def result = sel.select()                                       //Random selection
def result = sel()                                              //Random selection (a short-hand variant)
def result = sel.select([true, true, false, true])              //Random selection with guards specified
def result = sel([true, true, false, true])                     //Random selection with guards specified (a short-hand variant)

def result = sel.prioritySelect()                               //Priority selection
def result = sel.prioritySelect([true, true, false, true])      //Priority selection with guards specifies

默认情况下,Select 方法会阻塞调用者的处理,直到有值可以读取。另一种方法 selectToPromise()prioritySelectToPromise() 提供了一种方法来获取稍后可以选择的值的 Promise。通过返回的 Promise,您可以注册一个回调,以便在选择下一个值时异步调用它。

随机选择和优先级选择
1
2
3
4
5
6
7
def sel = select(a, b, c, d)

Promise result = sel.selectToPromise()                                       //Random selection
Promise result = sel.selectToPromise([true, true, false, true])              //Random selection with guards specified

Promise result = sel.prioritySelectToPromise()                               //Priority selection
Promise result = sel.prioritySelectToPromise([true, true, false, true])      //Priority selection with guards specifies
另一种方法?

或者,Select 方法可以将其值发送到声明的 MessageStream(例如 Actor),而不会阻塞调用者。

示例
1
2
3
4
5
6
7
8
9
10
def handler = actor {...}
def sel = select(a, b, c, d)

sel.select(handler)                                         //Random selection
sel(handler)                                                //Random selection (a short-hand variant)
sel.select(handler, [true, true, false, true])              //Random selection with guards specified
sel(handler, [true, true, false, true])                     //Random selection with guards specified (a short-hand variant)

sel.prioritySelect(handler)                                 //Priority selection
sel.prioritySelect(handler, [true, true, false, true])      //Priority selection with guards specifies

保护

Guards 允许调用者从选择中省略一些输入通道。Guards 指定为传递给 select()prioritySelect() 方法的布尔标志列表。

一个有用的过滤器工具
1
2
def sel = select(leaders, seniors, experts, juniors)
def teamLead = sel([true, true, false, false]).value        //Only 'leaders' and 'seniors' qualify for becoming a teamLead here

Guards 的典型用途是使 Selects 足够灵活,以适应用户状态的变化。

Guards 在选择值期间启用/禁用通道
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import groovyx.gpars.dataflow.DataflowQueue
import static groovyx.gpars.dataflow.Dataflow.select
import static groovyx.gpars.dataflow.Dataflow.task

/**
 * Demonstrates the ability to enable/disable channels during a value selection on a Select by providing boolean guards.
 */
final DataflowQueue operations = new DataflowQueue()
final DataflowQueue numbers = new DataflowQueue()

def t = task {
    final def select = select(operations, numbers)
    3.times {
        def instruction = select([true, false]).value
        def num1 = select([false, true]).value
        def num2 = select([false, true]).value
        final def formula = "$num1 $instruction $num2"
        println "$formula = ${new GroovyShell().evaluate(formula)}"
    }
}

task {
    operations << '+'
    operations << '+'
    operations << '*'
}

task {
    numbers << 10
    numbers << 20
    numbers << 30
    numbers << 40
    numbers << 50
    numbers << 60
}

t.join()

优先级选择

当某些通道在选择时应优先于其他通道时,应使用 prioritySelect 方法。

一个 prioritySelect 示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
/**
 * Here's a simply usecase for Priority Select. It monitors a set of input channels for values and makes these values
 * available on its output irrespective of their original input channel.
 *
 * Note that dataflow variables, queues and broadcasts can be combined for Select.
 *
 * Unlike plain select method call, the prioritySelect call gives precedence to input channels with lower index.
 * Available messages from high priority channels will be served before messages from lower-priority channels.
 * Messages received through a single input channel will have their mutual order preserved.
 *
 */
def critical = new DataflowVariable()
def ordinary = new DataflowQueue()
def whoCares = new DataflowQueue()

task {
    ordinary << 'All working fine'
    whoCares << 'I feel a bit tired'
    ordinary << 'We are on target'
}

task {
    ordinary << 'I have just started my work. Busy. Will come back later...'
    sleep 5000
    ordinary << 'I am done for now'
}

task {
    whoCares << 'Huh, what is that noise'
    ordinary << 'Here I am to do some clean-up work'
    whoCares << 'I wonder whether unplugging this cable will eliminate that nasty sound.'
    critical << 'The server room runs on UPS!'
    whoCares << 'The sound has disappeared'
}

def select = select([critical, ordinary, whoCares])

println 'Starting to monitor our IT department'

sleep 3000
10.times {println "Received: ${select.prioritySelect().value}"}

收集异步计算的结果

无论它们是 数据流任务活动对象的 方法 还是 异步函数,异步活动始终返回 PromisePromises 实现 SelectableChannel 接口,因此可以与其他 Promises 以及 read channels 一起传递给 selects 进行选择。

JavaCompletionService 类似,我们的 GPars Select 方法使您能够在每个异步活动可用时立即获得其结果。此外,我们可以使用 Select 来获取从并行运行的多个计算中的第一个/最快的结果。

如何选择最快的结果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import groovyx.gpars.dataflow.Promise
import groovyx.gpars.dataflow.Select
import groovyx.gpars.group.DefaultPGroup
/**
 * Demonstrates the use of dataflow tasks and selects to pick the fastest result of concurrently run calculations.
 */

final group = new DefaultPGroup()
group.with {
    Promise p1 = task {
        sleep(1000)
        10 * 10 + 1
    }
    Promise p2 = task {
        sleep(1000)
        5 * 20 + 2
    }
    Promise p3 = task {
        sleep(1000)
        1 * 100 + 3
    }

    final alt = new Select(group, p1, p2, p3)

    def result = alt.select()
    println "Result: " + result
}

超时

Select.createTimeout() 方法将在声明的时间段后创建一个绑定到值的 DataflowVariable。这可以在 Selects 中使用,以便它们在指定延迟后解除阻塞(恢复处理),如果所有其他通道在该时间之前都没有传递值。只需将 timeout channel 作为另一个输入通道传递给 Select

一个 Timeout Channel 有助于选择最快的答案
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import groovyx.gpars.dataflow.Promise
import groovyx.gpars.dataflow.Select
import groovyx.gpars.group.DefaultPGroup
/**
 * Demonstrates the use of dataflow tasks and selects to pick the fastest result of concurrently run calculations.
 */

final group = new DefaultPGroup()
group.with {
    Promise p1 = task {
        sleep(1000)
        10 * 10 + 1
    }
    Promise p2 = task {
        sleep(1000)
        5 * 20 + 2
    }
    Promise p3 = task {
        sleep(1000)
        1 * 100 + 3
    }

    final timeoutChannel = Select.createTimeout(500)

    final alt = new Select(group, p1, p2, p3, timeoutChannel)

    def result = alt.select()
    println "Result: " + result
}

取消

好的,所以我们有了答案。其他继续寻找答案的任务怎么样?如果我们需要在找到答案或超时后取消其他任务,那么最好的方法是设置一个标志,让我们的任务定期监控该标志。


有意地,DataflowVariablesTasks 中没有内置的取消机制

一个选择最快的答案并取消其他答案的示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import groovyx.gpars.dataflow.Promise
import groovyx.gpars.dataflow.Select
import groovyx.gpars.group.DefaultPGroup

import java.util.concurrent.atomic.AtomicBoolean

/**
 * Demonstrates the use of dataflow tasks and selects to pick the fastest result of concurrently run calculations.
 * It shows a waY to cancel the slower tasks once a result is known
 */

final group = new DefaultPGroup()
final done = new AtomicBoolean()

group.with {
    Promise p1 = task {
        sleep(1000)
        if (done.get()) return
        10 * 10 + 1
    }
    Promise p2 = task {
        sleep(1000)
        if (done.get()) return
        5 * 20 + 2
    }
    Promise p3 = task {
        sleep(1000)
        if (done.get()) return
        1 * 100 + 3
    }

    final alt = new Select(group, p1, p2, p3, Select.createTimeout(500))

    def result = alt.select()
    done.set(true)

    println "Result: " + result
}

运算符

Dataflow OperatorsSelectors 提供了一个完整的 Dataflow 实现,包括所有通常的仪式。

概念

完整的 Dataflow Concurrency 基于连接操作符和选择器的通道的概念。这些对象消耗来自输入通道的值,将它们转换为新值,并将新值输出到它们的输出通道。

Operators所有 输入通道都有值之前等待,才会开始处理它们,但 Selectors 只等待 任何 输入通道上的第一个可用值。

一个 Operator 示例
1
2
3
4
operator(inputs: [a, b, c], outputs: [d]) {x, y, z ->
    ...
    bindOutput 0, x + y + z
}
一个示例缓存
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
/**
 * CACHE
 *
 * Caches sites' contents. Accepts requests for url content, outputs the content. Outputs requests for download
 * if the site is not in cache yet.
 */
operator(inputs: [urlRequests], outputs: [downloadRequests, sites]) {request ->

    if (!request.content) {
        println "[Cache] Retrieving ${request.site}"

        def content = cache[request.site]

        if (content) {
            println "[Cache] Found in cache"
            bindOutput 1, [site: request.site, word:request.word, content: content]
        } else {
            def downloads = pendingDownloads[request.site]
            if (downloads != null) {
                println "[Cache] Awaiting download"
                downloads << request
            } else {
                pendingDownloads[request.site] = []
                println "[Cache] Asking for download"
                bindOutput 0, request
            }
        }

    } else {
        println "[Cache] Caching ${request.site}"

        cache[request.site] = request.content
        bindOutput 1, request

        def downloads = pendingDownloads[request.site]

        if (downloads != null) {
            for (downloadRequest in downloads) {
                println "[Cache] Waking up"
                bindOutput 1, [site: downloadRequest.site, word:downloadRequest.word, content: request.content]
            }
            pendingDownloads.remove(request.site)
        }
    }
}
异常处理说明

标准错误处理将错误消息打印到标准错误输出,并在操作符体内抛出未捕获异常时终止操作符。要更改行为,您可以注册自己的事件监听器。有关更多详细信息,请参见 Operator Lifecycle 部分。

异常处理示例
1
2
3
4
5
6
7
8
9
10
11
12
def listener = new DataflowEventAdapter() {

    @Override
    boolean onException(final DataflowProcessor processor, final Throwable e) {
        logChannel << e
        return false   //Indicate whether to terminate the operator or not
    }
}

op = group.operator(inputs: [a, b], outputs: [c], listeners: [listener]) {x, y ->
    ...
}

操作符类型

操作符方法有专门的版本,用于特定目的

  • operator - 基本的通用操作符

  • selector - 由任何输入通道上的值可用触发的操作符

  • prioritySelector - 一个选择器,它优先传递来自较低索引输入通道的消息,而不是来自较高索引通道的消息

  • splitter - 一个单输入操作符,将它的输入值复制到所有输出通道

将操作符连接在一起

操作符通常组合成网络,就像某些操作符消耗其他操作符生成的输出一样。

使用多个操作符
1
2
3
operator(inputs:[a, b], outputs:[c, d]) {...}
splitter(c, [e, f])
selector(inputs:[e, d]: outputs:[]) {...}

您也可以通过操作符本身来引用输出通道

一个更复杂的操作符示例
1
2
3
4
def op1 = operator(inputs:[a, b], outputs:[c, d]) {...}
def sp1 = splitter(op1.outputs[0], [e, f])                            //takes the first output of op1

selector(inputs:[sp1.outputs[0], op1.outputs[1]]: outputs:[]) {...}   //takes the first output of sp1 and the second output of op1

对操作符进行分组

Dataflow 操作符可以组织成组,以进行性能微调。组提供了一个方便的 operator() 工厂方法来创建附加到组的任务。

用于性能微调?使用 Groups
1
2
3
4
5
6
7
8
9
10
import groovyx.gpars.group.DefaultPGroup

def group = new DefaultPGroup()

group.with {
    operator(inputs: [a, b, c], outputs: [d]) {x, y, z ->
        ...
        bindOutput 0, x + y + z
    }
}
Dataflow 的自定义线程池

数据流操作符的默认线程池包含守护线程,这意味着您的应用程序将在主线程完成时立即退出,不会等待所有任务完成。

当对操作符进行分组时,请确保您的自定义线程池也使用守护线程,这可以通过使用 DefaultPGroup 或向线程池构造函数提供您自己的线程工厂来实现,或者如果您的线程池使用非守护线程(例如,当使用 NonDaemonPGroup 组类时),请确保您显式地关闭该组或线程池,方法是调用它的 shutdown() 方法,否则您的应用程序将无法退出。

您可以使用 Dataflow.usingGroup() 方法选择性地覆盖代码块中用于任务、操作符、回调和其他数据流元素的默认组

示例
1
2
3
4
5
6
Dataflow.usingGroup(group) {
    operator(inputs: [a, b, c], outputs: [d]) {x, y, z ->
        ...
        bindOutput 0, x + y + z
    }
}

您可以始终通过具体化来覆盖默认组

示例
1
2
3
4
5
6
Dataflow.usingGroup(group) {
    anotherGroup.operator(inputs: [a, b, c], outputs: [d]) {x, y, z ->
        ...
        bindOutput 0, x + y + z
    }
}

构造运算符

操作符的构造属性(如 inputsoutputsstateObjectmaxForks)在操作符构建后无法修改。当您最终构建操作符之前,您可能会发现 groovyx.gpars.dataflow.ProcessingNode 类在将通道和值逐渐收集到列表中时很有用。

示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import groovyx.gpars.dataflow.Dataflow
import groovyx.gpars.dataflow.DataflowQueue
import static groovyx.gpars.dataflow.ProcessingNode.node

/**
 * Shows how to build operators using the ProcessingNode class
 */

final DataflowQueue aValues = new DataflowQueue()
final DataflowQueue bValues = new DataflowQueue()
final DataflowQueue results = new DataflowQueue()

//Create a config and gradually set the required properties - channels, code, etc.
def adderConfig = node {valueA, valueB ->
    bindOutput valueA + valueB
}
adderConfig.inputs << aValues
adderConfig.inputs << bValues
adderConfig.outputs << results

//Build the operator
final adder = adderConfig.operator(Dataflow.DATA_FLOW_GROUP)

//Now the operator is running and processing the data
aValues << 10
aValues << 20
bValues << 1
bValues << 2

assert [11, 22] == (1..2).collect {
    results.val
}

在运算符中保存状态

尽管操作符通常可以在没有状态的情况下执行后续调用,但 GPars 允许操作符根据开发人员的要求保持状态。一个显而易见的方法是利用 Groovy 闭包功能来闭合其上下文

示例
1
2
3
4
int counter = 0
operator(inputs: [a], outputs: [b]) {value ->
    counter += 1
}

另一种方法是将状态对象作为 stateObject 参数在构造时传递给操作符,这样可以避免在操作符定义之外声明状态对象

示例
1
2
3
operator(inputs: [a], outputs: [b], stateObject: [counter: 0]) {value ->
    stateObject.counter += 1
}

并行化运算符

默认情况下,操作符的主体一次由一个线程处理。虽然这是一个安全设置,允许操作符的主体以非线程安全的方式编写,但一旦操作符变得“热门”,并且数据开始在操作符的输入队列中累积,您可能需要考虑允许多个线程同时运行操作符的主体。请记住,在这种情况下,您需要避免或保护共享资源免受多线程访问。要允许多个线程同时运行操作符的主体,请在创建操作符时传递额外的 maxForks 参数

示例
1
2
3
4
def op = operator(inputs: [a, b, c], outputs: [d, e], maxForks: 2) {x, y, z ->
    bindOutput 0, x + y + z
    bindOutput 1, x * y * z
}

maxForks 参数的值指示同时运行操作符的线程的最大数量。只允许正数,默认值为 1。

线程饥饿

请始终确保为操作符提供服务的 group 包含足够多的线程来支持所有请求的分叉。使用组允许您围绕不同的线程池(包装在组内)组织任务或操作符。虽然 Dataflow.task() 命令在默认线程池(java.util.concurrent.Executor,固定大小 = #cpu+1,守护线程)上调度任务,但您可能更愿意能够定义您自己的线程池来运行您的任务。

示例
1
2
def group = new DefaultPGroup(10)
group.operator((inputs: [a, b, c], outputs: [d, e], maxForks: 5) {x, y, z -> ...}

默认组使用可调整大小的线程池,因此永远不会用完线程。


同步输出

当通过将 maxForks 的值设置为大于 1 来启用操作符的内部并行化时,重要的是要记住,如果没有操作符主体中的显式或隐式同步,可能会发生竞争条件。尤其要注意,写入多个输出通道的值不能保证以相同的顺序原子地写入所有通道

示例
1
2
3
4
5
6
7
8
9
operator(inputs:[inputChannel], outputs:[a, b], maxForks:5) {msg ->
    bindOutput 0, msg
    bindOutput 1, msg
}
inputChannel << 1
inputChannel << 2
inputChannel << 3
inputChannel << 4
inputChannel << 5
May result in output channels having the values mixed-up something like:
示例
1
2
a -> 1, 3, 2, 4, 5
b -> 2, 1, 3, 5, 4
Explicit synchronization is one way to get correctly bound all output channels and protect operator not-thread local state:
示例
1
2
3
4
5
6
7
8
9
10
def lock = new Object()
operator(inputs:[inputChannel], outputs:[a, b], maxForks:5) {msg ->
    doStuffThatIsThreadSafe()

    synchronized(lock) {
        doSomethingThatMustNotBeAccessedByMultipleThreadsAtTheSameTime()
        bindOutput 0, msg
        bindOutput 1, 2*msg
    }
}

显然,您需要在这里权衡利弊,因为同步可能会破坏将 maxForks 设置为大于 1 的目的。

要在一项原子操作中设置所有操作符输出通道的值,您还可以考虑调用 bindAllOutputsAtomically 方法,将一个值传递给该方法以写入所有输出通道,或者调用 bindAllOutputsAtomically 方法,该方法接受多个值,每个值都将写入具有相同位置索引的输出通道。

示例
1
2
3
4
5
operator(inputs:[inputChannel], outputs:[a, b], maxForks:5) {msg ->
    doStuffThatIsThreadSafe()
        bindAllOutputValuesAtomically msg, 2*msg
    }
}
我使用哪种绑定?
Using the _bindAllOutputs_ or the _bindAllOutputValues_ methods will not guarantee atomicity of writes across al the output channels when using internal parallelism.

如果保留多个输出通道中消息的顺序不是问题,那么 bindAllOutputs 以及 bindAllOutputValues 将比原子变体提供更好的性能。

操作符生命周期

数据流操作符和选择器在它们的生命周期中触发几个事件,这使感兴趣的方能够获得通知并可能改变操作符的行为。DataflowEventListener 接口提供了几个回调方法

示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
public interface DataflowEventListener {
    /**
     * Invoked immediately after the operator starts by a pooled thread before the first message is obtained
     *
     * @param processor The reporting dataflow operator/selector
     */
    void afterStart(DataflowProcessor processor);

    /**
     * Invoked immediately after the operator terminates
     *
     * @param processor The reporting dataflow operator/selector
     */
    void afterStop(DataflowProcessor processor);

    /**
     * Invoked if an exception occurs.
     * If any of the listeners returns true, the operator will terminate.
     * Exceptions outside of the operator's body or listeners' messageSentOut() handlers will terminate the operator irrespective of the listeners' votes.
     *
     * @param processor The reporting dataflow operator/selector
     * @param e         The thrown exception
     * @return True, if the operator should terminate in response to the exception, false otherwise.
     */
    boolean onException(DataflowProcessor processor, Throwable e);

    /**
     * Invoked when a message becomes available in an input channel.
     *
     * @param processor The reporting dataflow operator/selector
     * @param channel   The input channel holding the message
     * @param index     The index of the input channel within the operator
     * @param message   The incoming message
     * @return The original message or a message that should be used instead
     */
    Object messageArrived(DataflowProcessor processor, DataflowReadChannel<Object> channel, int index, Object message);

    /**
     * Invoked when a control message (instances of ControlMessage) becomes available in an input channel.
     *
     * @param processor The reporting dataflow operator/selector
     * @param channel   The input channel holding the message
     * @param index     The index of the input channel within the operator
     * @param message   The incoming message
     * @return The original message or a message that should be used instead
     */
    Object controlMessageArrived(DataflowProcessor processor, DataflowReadChannel<Object> channel, int index, Object message);

    /**
     * Invoked when a message is being bound to an output channel.
     *
     * @param processor The reporting dataflow operator/selector
     * @param channel   The output channel to send the message to
     * @param index     The index of the output channel within the operator
     * @param message   The message to send
     * @return The original message or a message that should be used instead
     */
    Object messageSentOut(DataflowProcessor processor, DataflowWriteChannel<Object> channel, int index, Object message);

    /**
     * Invoked when all messages required to trigger the operator become available in the input channels.
     *
     * @param processor The reporting dataflow operator/selector
     * @param messages  The incoming messages
     * @return The original list of messages or a modified/new list of messages that should be used instead
     */
    List<Object> beforeRun(DataflowProcessor processor, List<Object> messages);

    /**
     * Invoked when the operator completes a single run
     *
     * @param processor The reporting dataflow operator/selector
     * @param messages  The incoming messages that have been processed
     */
    void afterRun(DataflowProcessor processor, List<Object> messages);

    /**
     * Invoked when the fireCustomEvent() method is triggered manually on a dataflow operator/selector
     *
     * @param processor The reporting dataflow operator/selector
     * @param data      The custom piece of data provided as part of the event
     * @return A value to return from the fireCustomEvent() method to the caller (event initiator)
     */
    Object customEvent(DataflowProcessor processor, Object data);
}

通过DataflowEventAdapter类提供了一个默认实现。

监听器提供了一种在操作符内部发生异常时处理异常的方法。监听器通常会记录此类异常,通知主管实体,生成替代输出,或执行从异常情况中恢复所需的任何步骤。如果没有注册监听器,或者任何监听器返回true,操作符将终止,保留afterStop()的契约。在实际操作符主体之外发生的异常,即在主体被触发之前的参数准备阶段,或在主体完成之后清理和通道订阅阶段,始终会导致操作符终止。

操作符和选择器上可用的fireCustomEvent()方法可用于在操作符主体和感兴趣的监听器之间进行通信。

示例
1
2
3
4
5
6
7
8
9
10
11
12
13
final listener = new DataflowEventAdapter() {
    @Override
    Object customEvent(DataflowProcessor processor, Object data) {
        println "Log: Getting quite high on the scale $data"
        return 100  //The value to use instead
    }
}

op = group.operator(inputs: [a, b], outputs: [c], listeners: [listener]) {x, y ->
    final sum = x + y
    if (sum > 100) bindOutput(fireCustomEvent(sum))  //Reporting that the sum is too high, binding the lowered value that comes back
    else bindOutput sum
}

选择器

选择器的主体应是一个闭包,它接受一个或两个参数。

示例
1
2
3
selector (inputs : [a, b, c], outputs : [d, e]) {value ->
    ....
}

两个参数的闭包将获取一个值加上当前正在处理的输入通道的索引。这允许选择器区分来自不同输入通道的值。

示例
1
2
3
selector (inputs : [a, b, c], outputs : [d, e]) {value, index ->
    ....
}

优先级选择器

当需要在输入通道之间保留优先级时,应使用DataflowPrioritySelector

示例
1
2
3
prioritySelector(inputs : [a, b, c], outputs : [d, e]) {value, index ->
    ...
}

优先级选择器始终优先考虑来自具有较低位置索引的通道的值,而不是来自具有较高位置索引的通道的值。


联接选择器

没有指定主体闭包的选择器将把所有传入的值复制到其所有输出通道。

示例
1
def join = selector (inputs : [programmers, analysis, managers], outputs : [employees, colleagues])

内部并行性

允许内部选择器并行的maxForks属性也可用。

示例
1
2
3
selector (inputs : [a, b, c], outputs : [d, e], maxForks : 5) {value ->
    ....
}

保护

Selects一样,Selectors也允许用户暂时将单个输入通道包含在选择中或从选择中排除。guards输入属性可用于在所有输入通道上设置初始掩码,然后选择器主体中可用setGuardssetGuard方法。

示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import groovyx.gpars.dataflow.DataflowQueue
import static groovyx.gpars.dataflow.Dataflow.selector
import static groovyx.gpars.dataflow.Dataflow.task

/**
 * Demonstrates the ability to enable/disable channels during a value selection on a select by providing boolean guards.
 */
final DataflowQueue operations = new DataflowQueue()
final DataflowQueue numbers = new DataflowQueue()

def instruction
def nums = []

selector(inputs: [operations, numbers], outputs: [], guards: [true, false]) {value, index ->   //initial guards is set here
    if (index == 0) {
        instruction = value
        setGuard(0, false)  //setGuard() used here
        setGuard(1, true)
    }
    else nums << value
    if (nums.size() == 2) {
        setGuards([true, false])                                    //setGuards() used here
        final def formula = "${nums[0]} $instruction ${nums[1]}"
        println "$formula = ${new GroovyShell().evaluate(formula)}"
        nums.clear()
    }
}

task {
    operations << '+'
    operations << '+'
    operations << '*'
}

task {
    numbers << 10
    numbers << 20
    numbers << 30
    numbers << 40
    numbers << 50
    numbers << 60
}
警告

避免组合guards和大于1的maxForks。虽然Selector是线程安全的,不会以任何方式损坏,但guards可能不会按预期设置。多个线程并发运行选择器主体将倾向于互相覆盖对guards属性的设置。

关闭数据流网络

关闭数据流处理器(操作符和选择器)网络有时可能是一项非平凡的任务,尤其是在需要一种不会留下任何未处理消息的通用机制时。

数据流操作符和选择器可以通过三种方式终止

  • 通过对需要终止的所有操作符调用terminate()方法

  • 通过发送泊松消息

  • 通过建立活动监视器网络,该网络将在处理完所有消息后关闭网络

查看GPars提供的有关方法的详细信息。

关闭线程池

如果使用自定义PGroup为数据流网络维护线程池,则不应忘记在网络终止后关闭池。否则线程池将消耗系统资源,并且,在使用非守护线程的情况下,它将阻止JVM退出。

紧急关闭

可以对任何操作符/选择器调用terminate(),立即将其关闭。如果您跟踪所有处理器,也许可以通过将它们添加到列表中,停止网络的最快方法是

示例
1
allMyProcessors*.terminate()

但是,这应该被视为紧急退出,因为不能保证已处理的消息或完成的工作。操作符将立即终止,留下未完成的工作并放弃输入通道中的消息。当然,挂钩到操作符/选择器的生命周期事件监听器将按顺序调用其afterStop()事件处理程序,例如释放资源或将注释输出到日志中。

示例
1
2
3
4
5
6
7
8
9
10
def op1 = operator(inputs: [a, b, c], outputs: [d, e]) {x, y, z -> }

def op2 = selector(inputs: [d], outputs: [f, out]) { }

def op3 = prioritySelector(inputs: [e, f], outputs: [b]) {value, index -> }

[op1, op2, op3]*.terminate()  //Terminate all operators by calling the terminate() method on them
op1.join()
op2.join()
op3.join()

通过System.exit()关闭整个JVM显然会关闭数据流网络,但是不会调用任何生命周期监听器。

温和地停止操作符

操作符重复处理传入的消息。在操作符完成消息处理并即将在传入管道中寻找更多消息时,停止操作符而不必担心丢失任何消息的唯一安全时机。这正是terminateAfterNextRun()方法的作用。它将在处理完下一组消息后安排操作符关闭。

未处理的消息将保留在输入通道中,这允许您稍后处理它们,也许可以使用不同的操作符/选择器或其他方式。使用terminateAfterNextRun(),您不会丢失任何输入消息。当使用一组操作符/选择器来负载均衡来自通道的消息时,这可能特别方便。一旦工作负载减少,可以使用terminateAfterNextRun()方法安全地减少负载均衡操作符池。

检测关闭

对于那些需要阻塞直到操作符终止的人,操作符和选举器提供了一个方便的join()方法。

示例
1
allMyProcessors*.join()

这是等待整个数据流网络关闭的最简单方法,无论使用哪种关闭方法。


毒丸

PoisonPill是使用专用消息停止接收它的实体的策略的常用术语。GPars提供PoisonPill类,它对操作符和选择器具有完全相同的效果。由于PoisonPill是一个ControlMessage,它对操作符主体不可见,自定义代码无需以任何方式处理它。DataflowEventListeners可以通过controlMessageArrived()处理程序方法对ControlMessages做出反应。

示例
1
2
3
4
5
6
7
8
9
10
11
def op1 = operator(inputs: [a, b, c], outputs: [d, e]) {x, y, z -> }

def op2 = selector(inputs: [d], outputs: [f, out]) { }

def op3 = prioritySelector(inputs: [e, f], outputs: [b]) {value, index -> }

a << PoisonPill.instance  //Send the poisson

op1.join()
op2.join()
op3.join()

操作符在收到毒丸后,将在完成当前计算并确保将毒丸发送到其所有输出通道后立即终止。这样毒丸就可以传播到连接的操作符。另外,虽然操作符通常等待所有输入具有值,但在PoisonPills的情况下,操作符将在其任何输入上出现PoisonPill时立即终止。从其他通道获得的值将丢失。如果这些消息应该被处理,那么这可以被认为是网络设计中的错误。它们需要一个适当的值作为它们的同行,而不是一个PoisonPill才能正常处理。

另一方面,选择器将耐心地等待从所有输入通道接收PoisonPill,然后再将其发送到输出通道。这种行为防止包含涉及选择器的反馈循环的网络使用PoisonPill关闭。选择器永远不会从来自选择器后面的通道接收PoisonPill。应针对此类网络使用其他关闭策略。

操作符和选择器应仅自行终止

鉴于操作符网络的潜在多样性和其异步性质,一个好的终止策略是操作符和选择器应仅自行终止。

所有从外部终止它们的方式(无论是通过调用terminate()方法还是通过在流中发送毒药)都可能导致消息在管道中的某个地方丢失。当读取操作符在完全处理其输入通道中等待的消息之前终止时,就会发生这种情况。

立即毒丸

特别是对于选择器在接收到毒丸后立即关闭,引入了立即毒丸的概念。由于普通的非立即毒丸只会关闭输入通道,使选择器保持活动状态,直到至少一个输入通道保持打开状态,立即毒丸会立即关闭选择器。显然,一旦选择器读取了立即毒丸,来自其他选择器输入通道的未处理消息将不会由选择器处理。

使用立即毒丸,您可以安全地关闭包含参与反馈循环的选择器的网络。

示例
1
2
3
4
5
6
7
def op1 = selector(inputs: [a, b, c], outputs: [d, e]) {value, index -> }
def op2 = selector(inputs: [d], outputs: [f, out]) { }
def op3 = prioritySelector(inputs: [e, f], outputs: [b]) {value, index -> }

a << PoisonPill.immediateInstance

[op1, op2, op3]*.join()

带计数的毒丸

当在操作符网络中发送毒丸时,您可能需要在所有操作符或指定数量的操作符停止时收到通知。CountingPoisonPill类完全满足此目的

示例
1
2
3
4
5
6
7
8
9
10
11
operator(inputs: [a, b, c], outputs: [d, e]) {x, y, z -> }
selector(inputs: [d], outputs: [f, out]) { }
prioritySelector(inputs: [e, f], outputs: [b]) {value, index -> }

//Send the poisson indicating the number of operators than need to be terminated before we can continue
final pill = new CountingPoisonPill(3)
a << pill

//Wait for all operators to terminate
pill.join()
//At least 3 operators should be terminated by now

CountingPoisonPill类的termination属性是一个常规的Promise<Boolean>,因此具有许多方便的属性。

示例
1
2
3
4
5
6
7
8
9
10
11
//Send the poisson indicating the number of operators than need to be terminated before we can continue
final pill = new CountingPoisonPill(3)
pill.termination.whenBound {println "Reporting asynchronously that the network has been stopped"}
a << pill

if (pill.termination.bound) println "Wow, that was quick. We are done already!"
else println "Things are being slow today. The network is still running."

//Wait for all operators to terminate
assert pill.termination.get()
//At least 3 operators should be terminated by now

CountingPoisonPill的立即变体也可用 - ImmediateCountingPoisonPill

示例
1
2
3
4
5
6
7
def op1 = selector(inputs: [a, b, c], outputs: [d, e]) {value, index -> }
def op2 = selector(inputs: [d], outputs: [f, out]) { }
def op3 = prioritySelector(inputs: [e, f], outputs: [b]) {value, index -> }

final pill = new ImmediateCountingPoisonPill(3)
a << pill
pill.join()

ImmediateCountingPoisonPill将安全且立即关闭数据流网络,即使包含参与反馈循环的选择器也是如此,而正常的非立即毒丸则无法做到。


毒丸策略

要使用PoisonPill正确关闭网络,您必须确定要发送PoisonPill的适当通道集。PoisonPill将通过通道和处理器以通常的方式在网络中传播到下游。通常,要发送PoisonPill的正确通道将是作为网络的数据源的那些通道。这对于一般情况或复杂网络可能很难实现。另一方面,对于具有主要消息流方向的网络,PoisonPill提供了一种非常直接的方法来优雅地关闭整个网络。

负载均衡阻止毒药关闭

负载均衡架构使用多个操作符从共享通道(队列)读取消息,也将阻止毒药关闭正常工作,因为只有一个读取操作符能够读取毒药消息。您可以考虑使用分叉操作符,将maxForks属性设置为大于1的值。另一种选择是手动将消息流拆分为多个通道,每个通道由一个原始操作符消耗。

终止技巧和窍门

请注意,GPars任务返回一个DataflowVariable,该变量在任务完成时绑定到一个值。下面的“终止符”操作符利用了DataflowVariablesDataflowReadChannel接口实现的事实,因此可以被操作符消耗。一旦两个任务都完成,操作符就会在q通道中发送一个PoisonPill来停止消费者,因为它会处理所有数据。

示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import groovyx.gpars.dataflow.DataflowQueue
import groovyx.gpars.group.NonDaemonPGroup


def group = new NonDaemonPGroup()

final DataflowQueue q = new DataflowQueue()

// final destination
def customs = group.operator(inputs: [q], outputs: []) { value ->
    println "Customs received $value"
}

// big producer
def green = group.task {
    (1..100).each {
        q << 'green channel ' + it
        sleep 10
    }
}

// little producer
def red = group.task {
    (1..10).each {
        q << 'red channel ' + it
        sleep 15
    }
}

def terminator = group.operator(inputs: [green, red], outputs: []) { t1, t2 ->
    q << PoisonPill.instance
}

customs.join()
group.shutdown()

将毒丸保留在给定的网络中

如果您的网络通过通道将值传递到网络之外的实体,则可能需要在网络边界停止PoisonPill消息。这可以通过在每个此类通道上放置一个单输入单输出过滤操作符来轻松实现。

示例
1
2
3
operator(networkLeavingChannel, otherNetworkEnteringChannel) {value ->
    if (!(value instanceOf PoisonPill)) bindOutput it
}

Pipeline DSL在这里也可能有所帮助

示例
1
networkLeavingChannel.filter { !(it instanceOf PoisonPill) } into otherNetworkEnteringChannel

查看Pipeline DSL部分以了解有关管道的更多信息。

优雅关闭

GPars提供了一种关闭数据流网络的通用方法。与前面提到的机制不同,这种方法将使网络保持运行,直到所有消息都被处理,然后优雅地关闭所有操作符,让您知道何时发生这种情况。但是,您必须付出适度的性能代价。这是不可避免的,因为我们需要跟踪网络内部发生的事情。

优雅的示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import groovyx.gpars.dataflow.DataflowBroadcast
import groovyx.gpars.dataflow.DataflowQueue
import groovyx.gpars.dataflow.operator.component.GracefulShutdownListener
import groovyx.gpars.dataflow.operator.component.GracefulShutdownMonitor
import groovyx.gpars.group.DefaultPGroup
import groovyx.gpars.group.PGroup

PGroup group = new DefaultPGroup(10)
final a = new DataflowQueue()
final b = new DataflowQueue()
final c = new DataflowQueue()
final d = new DataflowQueue<Object>()
final e = new DataflowBroadcast<Object>()
final f = new DataflowQueue<Object>()
final result = new DataflowQueue<Object>()

final monitor = new GracefulShutdownMonitor(100);

def op1 = group.operator(inputs: [a, b], outputs: [c], listeners: [new GracefulShutdownListener(monitor)]) {x, y ->
    sleep 5
    bindOutput x + y
}
def op2 = group.operator(inputs: [c], outputs: [d, e], listeners: [new GracefulShutdownListener(monitor)]) {x ->
    sleep 10
    bindAllOutputs 2*x
}
def op3 = group.operator(inputs: [d], outputs: [f], listeners: [new GracefulShutdownListener(monitor)]) {x ->
    sleep 5
    bindOutput x + 40
}
def op4 = group.operator(inputs: [e.createReadChannel(), f], outputs: [result], listeners: [new GracefulShutdownListener(monitor)]) {x, y ->
    sleep 5
    bindOutput x + y
}

100.times{a << 10}
100.times{b << 20}

final shutdownPromise = monitor.shutdownNetwork()

100.times{assert 160 == result.val}

shutdownPromise.get()
[op1, op2, op3, op4]*.join()

group.shutdown()

首先,我们需要一个GracefulShutdownMonitor的实例,它将协调关闭过程。它依赖于附加到所有操作符/选择器的GracefulShutdownListener实例。这些监听器观察它们各自的处理器以及它们的输入通道,并向共享的GracefulShutdownMonitor报告。一旦在GracefulShutdownMonitor上调用shutdownNetwork(),它将定期检查报告的活动,查询操作符的状态以及其输入通道中的消息数量。

请确保在启动关闭操作后,没有新的消息进入数据流网络,因为这会导致网络无法终止。只有在所有数据生产者停止向监视的网络发送更多消息后,才能启动关闭进程。

shutdownNetwork() 方法返回一个 Promise,以便您可以使用它进行通常的一系列操作——使用 get() 方法阻塞等待网络终止,使用 whenBound() 方法注册回调,或使用 then() 方法触发一系列活动。

优雅关闭的局限性
  • 为了使 GracefulShutdownListener 正确工作,它的 messageArrived() 事件处理程序必须看到通过输入通道到达的原始值。由于某些事件监听器可能会在消息通过监听器时更改消息,因此建议将 GracefulShutdownListener 首先添加到每个数据流处理器的监听器列表中。

  • 此外,对于那些在 controlMessageArrived() 事件处理程序中将控制消息转换为普通值消息的罕见操作符,优雅关闭将不起作用。

  • 第三,也是最后一点,使用多个操作符从共享通道(队列)中读取消息的负载均衡架构也会阻止优雅关闭正常工作。您可以考虑使用 分叉操作符,方法是将 maxForks 属性设置为大于 1 的值。另一种方法是手动将消息流拆分为多个通道,每个通道由一个原始操作符使用。

应用程序框架

数据流操作符 和 `Selectors1 可以成功地用于构建针对自然适合流模型的问题的高级领域特定框架。

GPars 数据流 之上构建流框架

GPars 数据流可以被视为底层语言级基础设施。

操作符、选择器、通道和事件监听器在语言级非常有用,例如,可以与 actor 或并行集合结合使用。每当需要异步处理通过一个或多个通道传入的事件时,数据流操作符或一个小数据流网络可能非常适合。与任务不同,操作符是轻量级的,并在没有消息要处理时释放线程。与 actor 不同,操作符通过通道间接寻址,并且可以轻松地将来自多个通道的消息组合到一个操作中。

或者,操作符可以被视为连续函数,它们即时且重复地将输入值转换为输出。我们认为,支持并发的通用编程语言应该提供这种类型的抽象。

同时,数据流元素可以很容易地用作构建域特定工作流式框架的构建块。这些框架可以提供专门针对单个问题领域的更高级抽象,这对于通用语言级库来说是不合适的。然后,每个更高层的概念都被映射到(可能几个)GPars 概念。

例如,解决数据挖掘问题的网络可能由多个数据源、数据清洗节点、分类节点、报告节点等组成。另一方面,图像处理网络可能需要专门用于图像压缩和格式转换的节点。类似地,用于数据加密、mp3 编码、工作流管理以及许多其他领域(这些领域将从基于数据流的解决方案中受益)的网络。这些网络将在许多方面有所不同——网络中节点的类型、事件的类型和频率、负载均衡方案、分支的潜在约束、对可视化、调试和日志记录的需求、用户定义网络和与网络交互的方式以及许多其他方面。

更高层的应用程序特定框架应该努力提供最适合给定领域的抽象,并隐藏 GPars 的复杂性。

例如,用户在屏幕上操作的网络的可视化图形通常不应显示参与网络的所有通道。调试或日志记录通道(很少对解决方案的核心做出贡献)是首先考虑排除的良好候选者之一。同样,协调负载均衡或优雅关闭等方面的通道和生命周期事件监听器可能不会暴露给用户,尽管它们将成为生成的和执行的网络的一部分。

同样,域特定模型中的单个通道实际上将转换为多个通道,可能带有一个或多个日志记录/转换/过滤操作符将它们连接在一起。与节点关联的函数很可能将被一些额外的基础设施代码包装起来,以形成操作符的主体。

GPars 为您提供了底层组件,这些组件可能会被应用程序特定框架完全抽象化。这使得 GPars 与域无关且通用,但在实现级别仍有用处。


管道 DSL

用于构建操作符管道的 DSL

构建数据流网络可以进一步简化。GPars 为构建(主要是线性)操作符管道提供了方便的快捷方式。

示例
1
2
3
4
5
6
7
8
9
10
def toUpperCase = {s -> s.toUpperCase()}

final encrypt = new DataflowQueue()
final DataflowReadChannel encrypted = encrypt | toUpperCase | {it.reverse()} | {'###encrypted###' + it + '###'}

encrypt << "I need to keep this message secret!"
encrypt << "GPars can build linear operator pipelines really easily"

println encrypted.val
println encrypted.val

这可以节省您直接创建、连接和操作所有形成管道的通道和操作符的麻烦。pipe 操作符允许您将一个函数/操作符/进程的输出连接到另一个函数/操作符/进程的输入。就像在命令行上链接系统进程一样。

pipe 操作符是更通用的 chainWith() 方法的方便简写。

示例
1
2
3
4
5
6
7
8
9
10
def toUpperCase = {s -> s.toUpperCase()}

final encrypt = new DataflowQueue()
final DataflowReadChannel encrypted = encrypt.chainWith toUpperCase chainWith {it.reverse()} chainWith {'###encrypted###' + it + '###'}

encrypt << "I need to keep this message secret!"
encrypt << "GPars can build linear operator pipelines really easily"

println encrypted.val
println encrypted.val

将管道与直接操作符结合使用

由于每个操作符管道都有一个入口通道和一个出口通道,因此可以将管道连接到更复杂的操作符网络中。只有您的想象力可以限制您在同一个网络定义中将管道与通道和操作符混合的能力。

示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def toUpperCase = {s -> s.toUpperCase()}
def save = {text ->
    //Just pretending to be saving the text to disk, database or whatever
    println 'Saving ' + text
}

final toEncrypt = new DataflowQueue()
final DataflowReadChannel encrypted = toEncrypt.chainWith toUpperCase chainWith {it.reverse()} chainWith {'###encrypted###' + it + '###'}

final DataflowQueue fork1 = new DataflowQueue()
final DataflowQueue fork2 = new DataflowQueue()
splitter(encrypted, [fork1, fork2])  //Split the data flow

fork1.chainWith save  //Hook in the save operation

//Hook in a sneaky decryption pipeline
final DataflowReadChannel decrypted = fork2.chainWith {it[15..-4]} chainWith {it.reverse()} chainWith {it.toLowerCase()}
      .chainWith {'Groovy leaks! Check out a decrypted secret message: ' + it}

toEncrypt << "I need to keep this message secret!"
toEncrypt << "GPars can build operator pipelines really easy"

println decrypted.val
println decrypted.val
通道类型的保留

通道的类型在整个管道中保留。例如,如果您从同步通道开始链接,则管道中的所有通道都将是同步的。在这种情况下,很明显,整个链都将阻塞,包括写入通道头的写入器,直到有人从管道尾部读取数据。

示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
final SyncDataflowQueue queue = new SyncDataflowQueue()
final result = queue.chainWith {it * 2}.chainWith {it + 1} chainWith {it * 100}

Thread.start {
    5.times {
        println result.val
    }
}

queue << 1
queue << 2
queue << 3
queue << 4
queue << 5

连接管道

可以使用 into() 方法连接两个管道(或通道)。

示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
final encrypt = new DataflowQueue()
final DataflowWriteChannel messagesToSave = new DataflowQueue()
encrypt.chainWith toUpperCase chainWith {it.reverse()} into messagesToSave

task {
    encrypt << "I need to keep this message secret!"
    encrypt << "GPars can build operator pipelines really easy"
}

task {
    2.times {
        println "Saving " + messagesToSave.val
    }
}

encryption 管道的输出直接连接到 saving 管道的输入(在本例中为单个通道)。

分叉 数据流

当需要将管道/通道的输出复制到多个后续管道/通道时,split() 方法将帮助您。

示例
1
2
3
4
5
final encrypt = new DataflowQueue()
final DataflowWriteChannel messagesToSave = new DataflowQueue()
final DataflowWriteChannel messagesToLog = new DataflowQueue()

encrypt.chainWith toUpperCase chainWith {it.reverse()}.split(messagesToSave, messagesToLog)

进入管道

split() 一样,tap() 方法允许您将数据流分叉到多个通道。然而,在某些情况下,轻触更方便,因为它将两个新分支中的一个视为管道的后续分支。

示例
1
queue.chainWith {it * 2}.tap(logChannel).chainWith{it + 1}.tap(logChannel).into(PrintChannel)

合并通道

合并允许您将多个读取通道作为单个数据流操作符的输入。作为第二个参数传递的函数需要接受与正在合并的通道数量相同的参数——每个参数都将保存相应通道的值。

示例
1
maleChannel.merge(femaleChannel) {m, f -> m.marry(f)}.into(mortgageCandidatesChannel)

分离

分离合并 的相反操作。提供的闭包返回一个值列表,每个值都将输出到具有相应位置索引的输出通道。

示例
1
queue1.separate([queue2, queue3, queue4]) {a -> [a-1, a, a+1]}

选择

binaryChoice()choice() 方法允许您将值发送到两个(或多个)输出通道中的一个,如闭包的返回值所示。

示例
1
2
queue1.binaryChoice(queue2, queue3) {a -> a > 0}
queue1.choice([queue2, queue3, queue4]) {a -> a % 3}

过滤

filter() 方法允许使用布尔谓词过滤管道中的数据。

示例
1
2
3
4
5
6
7
8
9
10
        final DataflowQueue queue1 = new DataflowQueue()
        final DataflowQueue queue2 = new DataflowQueue()

        final odd = {num -> num % 2 != 0 }

        queue1.filter(odd) into queue2
        (1..5).each {queue1 << it}
        assert 1 == queue2.val
        assert 3 == queue2.val
        assert 5 == queue2.val

空值

如果链式函数返回一个 null 值,它通常会作为有效值传递到管道中。要指示操作符不应将值进一步传递到管道中,必须返回一个 NullObject.nullObject 实例。

示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
        final DataflowQueue queue1 = new DataflowQueue()
        final DataflowQueue queue2 = new DataflowQueue()

        final odd = {num ->
            if (num == 5) return null  //null values are normally passed on
            if (num % 2 != 0) return num
            else return NullObject.nullObject  //this value gets blocked
        }

        queue1.chainWith odd into queue2
        (1..5).each {queue1 << it}
        assert 1 == queue2.val
        assert 3 == queue2.val
        assert null == queue2.val

自定义线程池

所有 Pipeline DSL 方法都允许指定自定义线程池或 PGroups

示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
channel | {it * 2}

channel.chainWith(closure)
channel.chainWith(pool) {it * 2}
channel.chainWith(group) {it * 2}

channel.into(otherChannel)
channel.into(pool, otherChannel)
channel.into(group, otherChannel)

channel.split(otherChannel1, otherChannel2)
channel.split(otherChannels)
channel.split(pool, otherChannel1, otherChannel2)
channel.split(pool, otherChannels)
channel.split(group, otherChannel1, otherChannel2)
channel.split(group, otherChannels)

channel.tap(otherChannel)
channel.tap(pool, otherChannel)
channel.tap(group, otherChannel)

channel.merge(otherChannel)
channel.merge(otherChannels)
channel.merge(pool, otherChannel)
channel.merge(pool, otherChannels)
channel.merge(group, otherChannel)
channel.merge(group, otherChannels)

channel.filter( otherChannel)
channel.filter(pool, otherChannel)
channel.filter(group, otherChannel)

channel.binaryChoice( trueBranch, falseBranch)
channel.binaryChoice(pool, trueBranch, falseBranch)
channel.binaryChoice(group, trueBranch, falseBranch)

channel.choice( branches)
channel.choice(pool, branches)
channel.choice(group, branches)

channel.separate( outputs)
channel.separate(pool, outputs)
channel.separate(group, outputs)

覆盖默认的 PGroup

为了避免必须为每个 Pipeline DSL 方法单独指定 PGroup,您可以覆盖默认数据流 PGroup 的值。

示例
1
2
3
4
5
Dataflow.usingGroup(group) {
    channel.choice(branches)
}
//Is identical to
channel.choice(group, branches)

Dataflow.usingGroup() 方法将给定代码块的默认数据流 PGroup 的值重置为指定的值。


管道构建器

Pipeline 类为操作符管道提供了一个直观的构建器。与直接链接通道相比,使用 Pipeline 类最大的好处是易于将自定义线程池/组应用于构建的链中的所有操作符。可用的方法和重载运算符与直接在通道上可用的方法和运算符相同。


使用 Pipeline 类最大的好处是易于使用。

示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import groovyx.gpars.dataflow.DataflowQueue
import groovyx.gpars.dataflow.operator.Pipeline
import groovyx.gpars.scheduler.DefaultPool
import groovyx.gpars.scheduler.Pool

final DataflowQueue queue = new DataflowQueue()
final DataflowQueue result1 = new DataflowQueue()
final DataflowQueue result2 = new DataflowQueue()
final Pool pool = new DefaultPool(false, 2)

final negate = {-it}

final Pipeline pipeline = new Pipeline(pool, queue)

pipeline | {it * 2} | {it + 1} | negate
pipeline.split(result1, result2)

queue << 1
queue << 2
queue << 3

assert -3 == result1.val
assert -5 == result1.val
assert -7 == result1.val

assert -3 == result2.val
assert -5 == result2.val
assert -7 == result2.val

pool.shutdown()

通过管道 DSL 传递构造参数

您可能经常需要能够将额外的初始化参数传递给操作符,例如要附加的监听器或 maxForks 的值。就像直接构建操作符时一样,Pipeline DSL 方法接受一个可选的参数映射以传递。

示例
1
new Pipeline(group, queue1).merge([maxForks: 4, listeners: [listener]], queue2) {a, b -> a + b}.into queue3

实现

GPars 中的数据流并发建立在与 Actor 支持相同的原则之上。所有数据流任务共享一个线程池,因此通过 Dataflow.task() 工厂方法创建的线程数量不必对应于系统所需的物理线程数量。PGroup.task() 工厂方法可以用于将创建的任务附加到一个组。由于每个组都定义了自己的线程池,因此您可以像使用 actor 一样轻松地围绕不同的线程池组织任务。

组合 Actor数据流并发

好消息是,您可以根据自己对特定问题的需要,以任何方式组合 actor数据流并发。您可以自由地在 Actor 中使用数据流变量。

示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
final DataflowVariable a = new DataflowVariable()

final Actor doubler = Actors.actor {
    react {message->
        a << 2 * message
    }
}

final Actor fakingDoubler = actor {
    react {
        doubler.send it  //send a number to the doubler
        println "Result ${a.val}"  //wait for the result to be bound to 'a'
    }
}

fakingDoubler << 10

在示例中,您可以看到 fakingDoubler 使用消息和 DataflowVariable 来与 doubler Actor 通信。


使用普通的 Java 线程

DataflowVariableDataflowQueue 类显然可以从应用程序的任何线程中使用,而不仅仅是从 Dataflow.task() 创建的任务中使用。请考虑以下示例。

DataflowVariable 示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import groovyx.gpars.dataflow.DataflowVariable

final DataflowVariable a = new DataflowVariable<String>()
final DataflowVariable b = new DataflowVariable<String>()

Thread.start {
    println "Received: $a.val"
    Thread.sleep 2000
    b << 'Thank you'
}

Thread.start {
    Thread.sleep 2000
    a << 'An important message from the second thread'
    println "Reply: $b.val"
}

我们正在创建两个普通的 java.lang.Thread 实例,它们使用两个数据流变量交换数据。显然,在这种情况下,Actor 生命周期方法、发送/反应功能或线程池都不会生效。


同步变量和通道

当使用异步数据流通道时,除了读取器必须等待值可用以供使用之外,通信双方仍然完全独立。写入器不会等待他们的消息被使用。读取器在值到达并请求时立即获取值。另一方面,同步通道可以同步写入器与读取器,以及多个读取器之间。

这在您需要提高确定性级别时特别有用。

异步通信强加的写入器到读取器的部分排序在使用同步通信时辅以读取器到写入器的部分排序。换句话说,可以保证,读取器在从同步通道读取值之前所做的任何操作都先于写入器在写入值之后所做的任何操作。此外,使用同步通信时,写入器永远不会领先于读取器太多,这简化了对系统的推理,并减少了管理数据生产速度以避免系统过载的需要。

同步数据流队列

SyncDataflowQueue 类应该用于点对点(1:1 或 n:1)通信。写入队列的每个消息都将被一个读取器使用。写入器被阻塞,直到他们的消息被使用,读取器被阻塞,直到有一个值可供他们读取。

同步通道阻塞写入器和读取器,直到所有各方都准备就绪。

同步通道示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import groovyx.gpars.dataflow.SyncDataflowQueue
import groovyx.gpars.group.NonDaemonPGroup

/**
 * Shows how synchronous dataflow queues can be used to throttle fast producer
 * when serving data to a slow consumer. Unlike when using asynchronous channels,
 * synchronous channels block both the writer and the readers until all parties
 * are ready to exchange messages.
 */

def group = new NonDaemonPGroup()

final SyncDataflowQueue channel = new SyncDataflowQueue()

def producer = group.task {
    (1..30).each {
        channel << it
        println "Just sent $it"
    }
    channel << -1
}

def consumer = group.task {
    while (true) {
        sleep 500  //simulating a slow consumer
        final Object msg = channel.val
        if (msg == -1) return
        println "Received $msg"
    }
}

consumer.join()

group.shutdown()

同步数据流广播

SyncDataflowBroadcast 类应该用于发布-订阅(1:n 或 n:m)通信。

发送到广播的每条消息将被所有订阅的读取器消费。写入器会被阻塞,直到他们的消息被所有读取器消费,读取器会被阻塞,直到有一个值可以供他们读取,并且所有其他订阅的读取器也请求该消息。使用 *SyncDataflowBroadcast*,您可以让所有读取器同时处理相同的消息,并在获取下一个消息之前相互等待。

SyncDataflowBroadcast 示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import groovyx.gpars.dataflow.SyncDataflowBroadcast
import groovyx.gpars.group.NonDaemonPGroup

/**
 * Shows how synchronous dataflow broadcasts can be used to throttle fast producer
 * when serving data to slow consumers. Unlike when using asynchronous channels,
 * synchronous channels block both the writer and the readers
 * until all parties are ready to exchange messages.
 */

def group = new NonDaemonPGroup()

final SyncDataflowBroadcast channel = new SyncDataflowBroadcast()

def subscription1 = channel.createReadChannel()
def fastConsumer = group.task {
    while (true) {
        sleep 10  //simulating a fast consumer
        final Object msg = subscription1.val
        if (msg == -1) return
        println "Fast consumer received $msg"
    }
}

def subscription2 = channel.createReadChannel()
def slowConsumer = group.task {
    while (true) {
        sleep 500  //simulating a slow consumer
        final Object msg = subscription2.val
        if (msg == -1) return
        println "Slow consumer received $msg"
    }
}

def producer = group.task {
    (1..30).each {
        println "Sending $it"
        channel << it
        println "Sent $it"
    }
    channel << -1
}

[fastConsumer, slowConsumer]*.join()

group.shutdown()

同步数据流变量

*DataflowVariable* 是异步的,只阻塞读取器直到变量绑定了一个值,而 *SyncDataflowVariable* 类提供了一种一次性数据交换机制,阻塞写入器和所有读取器,直到达到指定数量的等待方。

示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import groovyx.gpars.dataflow.SyncDataflowVariable
import groovyx.gpars.group.NonDaemonPGroup

final NonDaemonPGroup group = new NonDaemonPGroup()

//two readers required to exchange the message
final SyncDataflowVariable value = new SyncDataflowVariable(2)

def writer = group.task {
    println "Writer about to write a value"
    value << 'Hello'
    println "Writer has written the value"
}

def reader = group.task {
    println "Reader about to read a value"
    println "Reader has read the value: ${value.val}"
}

def slowReader = group.task {
    sleep 5000
    println "Slow reader about to read a value"
    println "Slow reader has read the value: ${value.val}"
}

[reader, slowReader]*.join()

group.shutdown()

看板流程

表 1. 看板流程 API 链接
API 链接 API 链接 API 链接 API 链接

看板流程

看板链接

看板托盘

处理节点

KanbanFlow 描述

*KanbanFlow* 是一个组合对象,它使用数据流抽象来定义多个并发生产者和消费者操作符之间的依赖关系。

生产者和消费者之间的每个链接由一个 *KanbanLink* 定义。

在每个 *KanbanLink* 内部,生产者和消费者之间的通信遵循 *KanbanFlow* 模式,如 看板流程模式 中所述。他们使用类型为 *KanbanTray* 的对象来将产品发送到下游,并向生产者发出对更多产品的请求信号。

下图说明了一个 *KanbanLink*,它有一个生产者、一个消费者和五个编号为 0 到 4 的托盘。托盘编号 0 已被用来将产品从生产者转移到消费者,已被消费者清空,现在被送回生产者的输入队列。托盘 1 和 2 等待运送等待消费的产品,而托盘 3 和 4 等待生产者使用。

dataflow kanban

*KanbanFlow* 对象将生产者链接到消费者,从而创建 *KanbanLink* 对象。在此过程中,可能会构建第二个链接,其中生产者与以前创建的链接中充当消费者的相同对象相同,因此两个链接连接起来形成一个链。

这是一个只有单个链接的 *KanbanFlow* 示例,例如,一个生产者和一个消费者。生产者始终将数字 1 发送到下游,而消费者打印此数字。

看板示例
1
2
3
4
5
6
7
8
9
10
11
12
import static groovyx.gpars.dataflow.ProcessingNode.node
import groovyx.gpars.dataflow.KanbanFlow

def producer = node { down -> down 1 }
def consumer = node { up   -> println up.take() }

new KanbanFlow().with {
    link producer to consumer
    start()
    // run for a while
    stop()
}

要将产品放入托盘并将托盘发送到下游,可以使用 **send()** 方法、**<<** 运算符或将托盘用作方法对象。以下行是等效的

等效选择示例
1
2
3
node { down -> down.send 1 }
node { down -> down << 1 }
node { down -> down 1 }

当使用 **take()** 方法从输入托盘中取出产品时,空托盘会自动释放。


您应该只调用一次 **take()**!

如果您不想使用空托盘将产品发送到下游(通常在 *ProcessingNode* 充当过滤器时是这样),则必须释放托盘以使其保持在活动状态。否则,系统中的托盘数量会减少。

您可以通过调用 **release()** 方法或使用 **~** 运算符(想想“把它甩掉”)来释放托盘。以下行是等效的

更多等效选择
1
2
node { down -> down.release() }
node { down -> ~down }
托盘释放

如果您调用任何 **take()** 或 **send()** 方法,托盘会自动释放。

各种链接结构

除了线性链之外,*KanbanFlow* 还可以将单个生产者链接到多个消费者(如树)或将多个生产者链接到单个消费者(收集器),或上述任何组合,从而导致有向无环图(**DAG**)。

*KanbanFlowTest* 类有许多此类结构的示例,包括一个生产者将工作委托给多个消费者的场景,其中

  • **工作窃取**策略,所有消费者都可以从下游选择。

  • **主从**策略,生产者从可用的消费者中选择。

  • **广播**策略,生产者将所有产品发送给所有消费者。

循环默认情况下是被禁止的,但如果启用,它们可以被用作所谓的生成器。生产者甚至可以是他自己的消费者,它在每个循环中都会增加产品价值。生成器本身保持无状态,因为该值只存储为托盘上的产品。这样的生成器可用于例如惰性序列或作为后续流程的 `heartbeat`。

生成器 `循环` 的方法可以同样应用于收集器,其中收集器不维护任何内部状态,而是将集合发送给自己,并在每次调用时添加产品。

一般来说,*ProcessingNode* 可以链接到自身以将状态导出到它发送给自身的托盘/产品中。然后,对产品的访问是 **按设计线程安全的**。


组合 KanbanFlows

就像 *KanbanLink* 对象可以链接在一起形成一个 *KanbanFlow* 一样,流本身也可以再次组合起来,从现有的较小的流中形成新的、更大的流。

看板流程示例
1
2
3
4
5
6
7
8
9
10
11
12
13
def firstFlow = new KanbanFlow()
def producer  = node(counter)
def consumer  = node(repeater)
firstFlow.link(producer).to(consumer)

def secondFlow = new KanbanFlow()
def producer2  = node(repeater)
def consumer2  = node(reporter)
secondFlow.link(producer2).to(consumer2)

flow = firstFlow + secondFlow

flow.start()

自定义并发特性

看板系统中的并发量由托盘数量决定(有时称为 **WIP** = *工作进行中*)。如果流中没有托盘,系统将不执行任何操作

  • 如果只有一个托盘,系统将被限制为顺序执行。

  • 如果有多个托盘,并发就会开始。

  • 如果托盘数量超过可用的处理单元,系统将开始浪费资源。

托盘数量可以通过多种方式控制。它们通常在启动流时设置。

设置托盘数量
1
2
3
flow.start(0) // start without trays
flow.start(1) // start with one tray per link in the flow
flow.start()  // start with the optimal number of trays

除了托盘之外,*KanbanFlow* 也可能受到其底层 *ThreadPool* 的限制。例如,大小为 **1** 的池将不允许太多并发。

*KanbanFlows* 使用默认池,该池由可用内核的数量确定。这可以通过设置 **pooledGroup** 属性来自定义。


经典示例

使用 `Dataflow Tasks` 实现埃拉托斯特尼筛法

埃拉托斯特尼筛法的示例解决方案
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
import groovyx.gpars.dataflow.DataflowQueue
import static groovyx.gpars.dataflow.Dataflow.task

/**
 * Demonstrates concurrent implementation of the Sieve of Eratosthenes using dataflow tasks
 */

final int requestedPrimeNumberCount = 1000

final DataflowQueue initialChannel = new DataflowQueue()

/**
 * Generating candidate numbers
 */
task {
    (2..10000).each {
        initialChannel << it
    }
}

/**
 * Chain a new filter for a particular prime number to the end of the Sieve
 * @param inChannel The current end channel to consume
 * @param prime The prime number to divide future prime candidates with
 * @return A new channel ending the whole chain
 */
def filter(inChannel, int prime) {
    def outChannel = new DataflowQueue()

    task {
        while (true) {
            def number = inChannel.val
            if (number % prime != 0) {
                outChannel << number
            }
        }
    }
    return outChannel
}

/**
 * Consume Sieve output and add additional filters for all found primes
 */
def currentOutput = initialChannel
requestedPrimeNumberCount.times {
    int prime = currentOutput.val
    println "Found: $prime"
    currentOutput = filter(currentOutput, prime)
}

同时使用 `Dataflow Tasks` 和 `Operators` 实现埃拉托斯特尼筛法

更复杂的解决方案
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
       import groovyx.gpars.dataflow.DataflowQueue
       import static groovyx.gpars.dataflow.Dataflow.operator
       import static groovyx.gpars.dataflow.Dataflow.task

       /**
        * Demonstrates concurrent implementation of the Sieve of Eratosthenes using dataflow tasks and operators
        */

       final int requestedPrimeNumberCount = 100

       final DataflowQueue initialChannel = new DataflowQueue()

       /**
        * Generating candidate numbers
        */
       task {
           (2..1000).each {
               initialChannel << it
           }
       }

       /**
        * Chain a new filter for a particular prime number to the end of the Sieve
        * @param inChannel The current end channel to consume
        * @param prime The prime number to divide future prime candidates with
        * @return A new channel ending the whole chain
        */
       def filter(inChannel, int prime) {
           def outChannel = new DataflowQueue()

           operator([inputs: [inChannel], outputs: [outChannel]]) {
               if (it % prime != 0) {
                   bindOutput it
               }
           }
           return outChannel
       }

       /**
        * Consume Sieve output and add additional filters for all found primes
        */
       def currentOutput = initialChannel
       requestedPrimeNumberCount.times {
           int prime = currentOutput.val
           println "Found: $prime"
           currentOutput = filter(currentOutput, prime)
       }

divider

用户指南:STM

软件事务内存

**软件事务内存**(**STM**)为开发人员提供了访问内存中数据的交易语义。这类似于数据库概念。

当多个线程共享内存中的数据时,通过将代码块标记为事务性(原子),开发人员将数据一致性责任委托给 **STM** 引擎。**GPars** 利用了 Multiverse **STM** 引擎

原子地运行一段代码

使用 **STM** 时,开发人员将代码组织成事务。事务是一段代码,它被 **原子地** 执行 - 既可以 **1)** 运行所有代码,也可以 **2)** 不运行任何代码。

无论事务是正常完成还是突然中止,事务使用的數據都保持 **一致性**。在事务内部运行时,代码会被赋予一种错觉,即它与其他并发运行的事务 **隔离**,因此一个事务对数据的更改在另一个事务提交之前不可见。这为我们提供了数据库事务的 **ACID** 特性的 **ACI** 部分。数据库中常见的 **持久性** 事务方面通常不被 **Stm** 强制。

**GPars** 允许开发人员使用 *atomic* 闭包来指定事务边界。

**ACI** 事务边界的示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import groovyx.gpars.stm.GParsStm
import org.multiverse.api.references.TxnInteger
import static org.multiverse.api.StmUtils.newTxnInteger

public class Account {
    private final TxnInteger amount = newTxnInteger(0);

    public void transfer(final int a) {
        GParsStm.atomic {
            amount.increment(a);
        }
    }

    public int getCurrentAmount() {
        GParsStm.atomicWithInt {
            amount.get();
        }
    }
}

*atomic* 闭包有几种类型,每种类型用于不同类型的返回值

  • *atomic* - 返回 *Object*

  • *atomicWithInt* - 返回 *int*

  • *atomicWithLong* - 返回 *long*

  • *atomicWithBoolean* - 返回 *boolean*

  • *atomicWithDouble* - 返回 *double*

  • *atomicWithVoid* - 没有返回值

默认情况下,**Multiverse** 使用乐观锁定策略并自动回滚和重试冲突的事务。

开发人员应该避免在事务代码中执行不可逆操作(例如,写入控制台、发送电子邮件、发射导弹等)。为了提高灵活性,可以通过自定义 *atomic block* 来自定义默认的 **Multiverse** 设置。

自定义事务属性

通常,希望为一些事务属性指定不同的值(例如,只读事务、锁定策略、隔离级别等)。*createAtomicBlock* 方法将创建一个新的 *AtomicBlock*,并使用提供的设置进行配置

使用自定义参数创建 **AtomicBlock**
1
2
3
4
5
6
7
8
import groovyx.gpars.stm.GParsStm
import org.multiverse.api.AtomicBlock
import org.multiverse.api.PropagationLevel

final TxnExecutor block = GParsStm.createTxnExecutor(maxRetries: 3000, familyName: 'Custom', PropagationLevel: PropagationLevel.Requires, interruptible: false)
assert GParsStm.atomicWithBoolean(block) {
    true
}

然后可以使用自定义的 *AtomicBlock* 使用指定的设置来创建事务。


*AtomicBlock* 实例是线程安全的,可以在线程和事务之间自由重复使用

使用 Transaction 对象

原子闭包使用当前 *Transaction* 作为参数。事务的 *Txn* 对象句柄可用于手动控制事务。这在下面的示例中得到说明,其中我们使用 *retry() * 方法来阻塞当前事务,直到计数器达到所需的值

示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import groovyx.gpars.stm.GParsStm
import org.multiverse.api.PropagationLevel
import org.multiverse.api.TxnExecutor

import static org.multiverse.api.StmUtils.newTxnInteger

final TxnExecutor block = GParsStm.createTxnExecutor(maxRetries: 3000, familyName: 'Custom', PropagationLevel: PropagationLevel.Requires, interruptible: false)

def counter = newTxnInteger(0)
final int max = 100

Thread.start {
    while (counter.atomicGet() < max) {
        counter.atomicIncrementAndGet(1)
        sleep 10
    }
}

assert max + 1 == GParsStm.atomicWithInt(block) { tx ->
    if (counter.get() == max) return counter.get() + 1
    tx.retry()
}

数据结构

您可能已经注意到,在前面的示例中,我们使用专用数据结构来保存值。事实上,普通的 **Java** 类不支持事务,因此不能直接使用,因为 **Multiverse** 无法在并发事务之间安全地共享它们,也不能提交或回滚它们。


普通的 **Java** 类不支持事务

我们需要使用了解事务的数据

  • TxnIntRef

  • TxnLongRef

  • TxnBooleanRef

  • TxnDoubleRef

  • TxnRef

通常,您可以通过 *org.multiverse.api.StmUtils* 类的工厂方法来创建它们。


更多信息

我们决定不重复 **Multiverse** 网站上已有的信息。

不幸的是,随着 Codehaus 的关闭,该网站已不再可用。您可以尝试从 Multiverse 源代码 中收集更多信息。

由于我们不清楚 **Multiverse** 项目的未来,因此我们将考虑在将来的 **GPars 2.0** 中使用其他 **STM** 实现。


divider

用户指南:GAE

Google App Engine 集成

**GPars** 可以在 Google App Engine(**GAE**) 上运行。它可以成为 **Groovy** 和 **Java** *GAE 应用程序* 的一部分,也可以被插入到 **Gaelyk** 中。


Google App Engine 被称为 **GAE**

小型 GPars App Engine 集成库 提供将 **GAE** 服务挂钩到 **GPars** 的所有必要基础设施。尽管您将在 **GAE** 线程上运行并利用 **GAE** 定时器服务,但高级抽象保持不变。在有限制的情况下,您仍然可以使用 **GPars** *actor、数据流、代理、并行集合* 等其他实用概念。

有关如何在 GAE 上使用 GPars 的详细信息,请参考 GPars App Engine 库


divider

用户指南:远程调用

诸如 ActorsDataflowsAgents 这样的概念并不局限于单个虚拟机。它们为并发编程提供了一个抽象层,使我们能够将逻辑与底层同步代码分离。这些概念可以轻松地扩展到网络中的多个节点。

以下说明描述了 GPars 中的 远程调用


GPars 的远程调用是 2014 年 Google 暑期代码 项目。

介绍

要远程使用 ActorsDataflowsAgents,引入了带有 Remote 前缀的新远程代理对象。

代理对象通常与其本地对应对象具有相同的接口。这使我们能够将它用作本地对应对象的替代。在幕后,代理对象只是将消息发送到原始实例。

为了跨网络传输消息,使用了 Netty 库。

为了创建代理对象,使用了实例序列化机制(更多信息请参见下面的 远程序列化)。

使用远程调用的总体方法如下(详细信息见下文)。

主机 A

  1. 创建远程调用上下文并启动服务器以处理传入请求。

  2. 在指定 名称 下发布实例。

主机 B

  1. 创建远程调用上下文。

  2. hostA:port 请求指定 名称 的实例。将返回一个 promise 对象。

  3. 从 promise 获取代理对象。


此时,每个请求都会创建一个新的连接。

远程序列化

使用以下机制创建代理对象。

对象 ←(序列化)→ 句柄 ---- [网络] ---- 句柄 ←(序列化)→ 代理对象

这种机制的主要优势之一是,发送回代理对象引用将被反序列化回原始实例。

由于所有消息在通过网络发送之前都会被序列化,因此它们必须实现 Serializable 接口。

这是使用内置 Java 序列化机制和 Netty ObjectDecoder/ObjectEncoder 的结果。另一方面,它使我们能够将任何自定义对象作为消息发送到 Actor 或使用任何类型的 DataflowVariable(s)。

数据流

为了对 Dataflows 使用远程调用,必须创建一个上下文(RemoteDataflows 类)。在这个上下文中,Dataflows 可以从远程主机发布和检索。

示例
1
def remoteDataflows = RemoteDataflows.create()

在所有小节中,我们假设上下文已如上所示创建。

创建上下文后,如果我们希望允许其他主机检索发布的 Dataflows,则需要启动一个服务器。我们需要提供要监听的地址和端口(例如:localhost:11222 或 10.0.0.123:11333)。

启动远程服务器
1
remoteDataflows.startServer HOST PORT

要停止服务器,我们有一个 stopServer() 方法。请注意,startstop 方法都是异步的,不会阻塞;服务器将在后台启动/停止。

多次执行这些方法或以错误的顺序执行它们将引发异常。


要仅从远程主机检索实例,启动服务器是不必要的。

DataflowVariable

DataflowVariableDataflows 子系统的一个核心部分,它获得了远程调用能力。其他结构(?)和子系统依赖于它。

在上下文中发布变量只需执行以下操作。

发布上下文
1
2
def variable = new DataflowVariable()
remoteDataflows.publish variable "my-first-variable"

这将在给定名称下注册变量,因此当收到请求的变量名称为 my-first-variable 时,该变量可以发送到远程主机。

重要的是要记住,在同一名称下发布另一个变量将覆盖先前的变量,后续请求将发送新发布的变量。

变量检索通过以下方法完成。

变量检索
1
2
def remoteVariablePromise = remoteDataflows.getVariable HOST, PORT, "my-first-variable"
def remoteVariable = remoteVariablePromise.get()

getVariable 方法是非阻塞的,它返回一个 promise 对象,该对象最终将保存指向该变量的代理对象的引用。该代理具有与 DataflowVariable 相同的接口,可以无缝地用作常规变量。

要查看完整示例,请参见我们的:groovyx.gpars.samples.remote.dataflow.variable 代码。


DataflowBroadcast

可以订阅远程主机上的 DataflowBroadcast。为此,我们必须先发布它(假设上下文已存在)。

DataflowBroadcast 示例
1
2
def stream = new DataflowBroadcast()
remoteDataflows.publish stream "my-first-broadcast"

然后在另一个主机上,可以检索它。

检索示例
1
2
def readChannelPromise = remoteDataflows.getReadChannel HOST, PORT, "my-first-broadcast"
def readChannel = readChannelPromise.get()

代理对象具有与 ReadChannel 相同的接口,可以与常规 DataflowBroadcastReadChannel 以相同的方式使用。

要查看完整示例,请参见:groovyx.gpars.samples.remote.dataflow.broadcast


DataflowQueue

DataflowQueue 功能获得了类似的功能,发布方式如下。

发布示例
1
2
def queue = new DataflowQueue()
remoteDataflows.publish queue, "my-first-queue"

以类似的方式,我们可以从远程主机检索它。

从远程来源检索
1
2
def queuePromise = remoteDataflows.getQueue HOST, PORT, "my-first-queue"
def queue = queuePromise.get()

可以将新项目推送到远程代理的队列中。这些元素将通过网络发送到原始实例并推送到原始实例中。

检索命令会向 原始 实例发送元素请求。

从概念上讲,远程代理是一个 接口 - 它只是向原始实例发送请求。

要查看完整示例,请参见:groovyx.gpars.samples.remote.dataflow.queuegroovyx.gpars.samples.remote.dataflow.queuebalancer


演员

远程 Actors 子系统以类似的方式设计。

要启动 RemoteActors 类,必须创建一个上下文。然后在这个上下文中,可以从远程主机发布或检索 Actors 实例。

远程创建
1
def remoteActors = RemoteActors.create()
发布
1
2
def actor = ...
remoteActors.publish actor, "actor-name"
检索
1
2
def actorPromise = remoteActors.get HOST, PORT, "actor-name"
def remoteActor = actorPromise.get()

可以加入远程 Actor,但这将阻塞,直到原始 Actor 完成其工作。发送回复和 sendAndWait 方法也受支持。

可以将任何对象作为消息发送到 Actor,但请记住,它必须是 Serializable

请参见示例:groovyx.gpars.samples.remote.actor


远程演员名称

RemoteActors 类上下文可以用名称标识。要使用名称创建一个上下文,请使用

创建命名上下文
1
def remoteActors = RemoteActors.create "test-group-1"

在这个上下文中发布的 Actors 可以通过提供特殊的 Actor URL 来访问。

例如:在这个上下文中,在名称为 actor 下发布 actor 使它可以通过 URL "test-group-1/actor" 访问。

一个命名示例
1
def actor = remoteActors.get "test-group-1/actor"

保存此 actor 的实例的主机和端口将自动确定。

调用 get 方法将向 255.255.255.255 发送广播查询,以搜索具有该特定名称的上下文中存在的 actor。匹配的实例将使用必要的信息(如主机和端口)响应该查询。

允许的 Actor 和上下文名称

由于 URL 可以包含 "\\"(反斜杠)作为上下文和 actor 名称之间的分隔符,因此我们不能在 actor 的名称中使用反斜杠,但上下文名称可以包含任何 UTF 字符。


代理

远程 Agent 系统以类似的方式设计。

首先,必须创建一个 RemoteAgents 类上下文。在这个上下文中,可以从远程主机发布或检索 Agents

远程创建示例
1
def remoteAgents = RemoteAgents.create()
发布
1
2
def agent = ...
remoteAgents.publish agent, "agent-name"
检索
1
2
def agentPromise = remoteAgents.get HOST, PORT, "agent-name"
def remoteAgent = agentPromise.get()

有两种方法可以执行用于更新远程 Agent 实例状态的闭包。

  • remote - 闭包被序列化并发送到原始实例并在该上下文中执行。

  • local - 检索当前状态,并在更新来源处执行闭包,然后将更新后的值发送到原始实例。对 Agent 的并发更改将等待此过程结束。

默认情况下,远程 Agent 使用 remote 执行策略,但如果需要,我们可以更改它。

将策略更改为 LOCAL
1
2
3
def agentPromise = remoteAgents.get HOST, PORT, "agent"
def remoteAgent =  agentPromise.get()
remoteAgent.executionPolicy = AgentClosureExecutionPolicy.LOCAL

divider

一般 GPars 技巧

分组

诸如 Agents、ActorsDataflow 任务和运算符之类的更高层次的并发概念可以围绕共享线程池进行分组。PGroup 类及其子类表示围绕线程池的便捷 GPars 包装器。使用该组的工厂方法创建的对象将共享该组的线程池。

xxxPGroup 示例
1
2
3
4
5
6
7
8
9
10
11
def group1 = new DefaultPGroup()
def group2 = new NonDaemonPGroup()

group1.with {
    task {...}
    task {...}
    def op = operator(...) {...}
    def actor = actor{...}
    def anotherActor = group2.actor{...}  //will belong to group2
    def agent = safe(0)
}
线程池组

自定义组的线程池时,请考虑使用现有的 GPars 实现 - DefaultPoolResizeablePool 类。或者,您可能希望创建自己的 groovyx.gpars.scheduler.Pool 接口实现,并将其传递给 DefaultPGroupNonDaemonPGroup 构造函数。

Java API

大多数 GPars 功能可以从 Java 以及从 Groovy 中使用。查看此 用户指南 中的 Java API - 从 Java 使用 GPars 部分。然后尝试基于 Maven 的独立 Java 演示应用程序。


随身携带 GPars

性能

您在 Groovy 中的代码可以与用 JavaScala 或任何其他编程语言编写的代码一样快。这并不奇怪,因为 GPars 从技术上讲是一个坚固美味的 Java 制作的蛋糕,上面覆盖着 Groovy DSL 糖霜。

然而,与 Java 不同,使用 GPars 以及其他 DSL 友好型语言,您很可能会免费体验到有用的代码加速。这种加速来自应用程序的更佳和更简洁的设计。

使用并发 DSL 编码将使您获得更小的代码库,该代码库使用并发原语作为语言结构。因此,构建健壮的并发应用程序、识别潜在的瓶颈或错误,然后消除它们要容易得多。

虽然整个 用户指南 都是描述如何使用 GroovyGPars 创建美观且健壮的并发代码,但我们希望利用其中的一些技巧来突出显示一些代码调整或轻微的设计折衷可以为您带来有趣的性能提升的地方。


并行集合

诸如 eachParallel()collectParallel() 等并行集合处理方法在幕后使用 并行数组,这是一种高效的树状数据结构。每次调用任何并行集合方法时,都必须从原始集合构建此数据结构。因此,当链接并行方法调用时,您可能考虑使用 map/reduce API,或者使用 ParallelArray API 直接避免 并行数组 创建开销。

并行查找示例
1
2
3
4
5
6
import groovyx.gpars.GParsPool;
GParsPool.withPool {
    people.findAllParallel{it.isMale()}.collectParallel{it.name}.any{it == 'Joe'}
    people.parallel.filter{it.isMale()}.map{it.name}.filter{it == 'Joe'}.size() > 0
    people.parallelArray.withFilter({it.isMale()} as Predicate).withMapping({it.name} as Mapper).any{it == 'Joe'} != null
}

在许多情况下,将池大小从默认值更改为其他值可以提高性能。特别是如果您的任务执行 IO 操作,例如文件或数据库访问、网络等。因此,增加池中的线程数量可能会提高性能。

增加池大小以提高性能
1
2
3
4
import groovyx.gpars.GParsPool;
GParsPool.withPool(50) {
    ...
}

由于您提供给并行集合处理方法的闭包会频繁且并发地执行,因此您可能会进一步略微受益于将它们转换为 Java。


演员

GPars actors 速度很快。DynamicDispatchActorsReactiveActors 速度大约是 DefaultActors 的两倍,因为它们不需要在后续消息到达之间维护隐式状态。事实上,DefaultActors 的性能与 Scala 中的 actors 相当,您很少听说 Scala 的 actors 速度很慢。


如果您追求最高性能,那么请识别代码中的模式!

如果您追求最高性能,那么一个好的开始是在您的 actor 代码中识别以下模式。

要查找的模式
1
2
3
4
5
6
7
8
9
10
actor {
    loop {
        react {msg ->
            switch(msg) {
                case String:...
                case Integer:...
            }
        }
    }
}
更好的替代方案:DynamicDispatchActor
1
2
3
4
messageHandler {
    when{String msg -> ...}
    when{Integer msg -> ...}
}

loopreact 方法的调用成本很高。

DynamicDispatchActorReactiveActor 定义为类而不是使用 messageHandlerreactor 工厂方法,也会为您带来一些速度提升。

动态示例
1
2
3
4
5
6
7
8
9
class MyHandler extends DynamicDispatchActor {
    public void handleMessage(String msg) {
        ...
    }

    public void handleMessage(Integer msg) {
        ...
    }
}

现在,将 MyHandler 类转换为 Java,以从 GPars 中挤出最后一点性能。


池调整

GPars 允许您将执行者(actor)分组到线程池中,让您能够自由地以任何方式组织执行者。 始终值得尝试不同的执行者池大小和类型。

FJPool 通常比 DefaultPool 具有更好的性能,但似乎对池中的线程数量更敏感。 有时候,使用 ResizeablePoolResizeableFJPool 可以通过自动消除不必要的线程来提高性能。

示例
1
2
3
4
5
6
def attackerGroup = new DefaultPGroup(new ResizeableFJPool(10))
def defenderGroup = new DefaultPGroup(new DefaultPool(5))

def attacker = attackerGroup.actor {...}
def defender = defenderGroup.messageHandler {...}
...

代理

GPars 代理(agent)在处理消息方面甚至比 执行者(actor)更快。 将 代理 围绕线程池进行合理分组的建议,以及调整池大小和类型的建议,同样适用于 代理执行者。 对于 代理,您还可以从提交用 Java 编写的闭包作为消息中获益。

分享您的经验

我们越了解 GPars 在实际中的应用,就越能更好地适应未来的发展。 请告诉我们您如何使用 GPars 以及它的性能如何。 向我们发送您的基准测试、性能比较或分析报告,帮助我们为您调整 GPars。 更多详情请参见 此页面


托管环境

托管环境,例如 Google App Engine,可能会对线程施加额外的限制。 为了让 GPars 更好地集成到这些环境中,可以自定义默认的线程工厂和计时器工厂。


Google App Engine 这样的托管环境会对线程施加限制

GPars_Config 类提供静态初始化方法,允许第三方注册他们自己的 PoolFactoryTimerFactory 接口的实现。 这些实现可以用来创建 执行者数据流(dataflow)和 PGroups 的默认池和计时器。

一些用于初始化对象的静态方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public final class GParsConfig {
    private static volatile PoolFactory poolFactory;
    private static volatile TimerFactory timerFactory;

    public static void setPoolFactory(final PoolFactory pool)

    public static PoolFactory getPoolFactory()

    public static Pool retrieveDefaultPool()

    public static void setTimerFactory(final TimerFactory timerFactory)

    public static TimerFactory getTimerFactory()

    public static GeneralTimer retrieveDefaultTimer(final String name, final boolean daemon)

    public static void shutdown()
}

自定义工厂应该在应用程序启动后立即注册,以便 执行者数据流 能够在他们的默认组中使用它们。


关闭

GParsConfig.shutdown() 方法可以在受管环境中使用,以便正确关闭所有异步运行的计时器并释放所有线程局部变量的内存。

在调用此方法后,GPars 库将无法再提供声明的服务。

兼容性

在托管环境中运行 GPars 时,可能会出现一些进一步的兼容性问题。

最明显的问题可能是 GAE 中缺少 ForkJoinThreadPool 支持。 因此,诸如 Fork/JoinGParsPool 之类的机制在某些服务上可能不可用。 但是,即使使用受管的非 Java SE 线程池,GParsExecutorsPool、Dataflow、Actors、AgentsSTM 也应该正常工作。


divider

用户指南:结论


这真是一个漫长的旅程,不是吗?

现在,在阅读完这份 用户指南 后,您已经准备好构建快速、健壮、可靠和并发性的应用程序了。

您已经看到了许多可供选择的概念,并且每个概念都有其适用的领域。 能够选择正确的概念应用是成为成功开发者的关键。

如果您认为可以用 GPars 完成这项工作,那么这份 用户指南 的使命就完成了。

现在,开始使用 GPars 并享受乐趣吧!