15.3 “线框–管道”模型
通常,设计和理解并发系统最好的方式是使用图形。我们将这种技术称为线框–管道(box-and-channel)模型。设想一个使用整型的简单场景,我们希望对之前计算f(x)+g(x)的例子做一个归纳。现在你想要使用参数x调用方法(或函数)p,并将计算的结果作为参数传递给函数q1和q2,接着使用这两个调用的结果去调用方法(或函数)r,然后打印结果(为了避免混乱,这里不再区分类C的方法m以及它关联的函数C::m)。这个任务用图形方式表示非常简单,如图15-7所示。

图 15-7 一个简单的“线框–管道”图
我们看看用Java实现图15-7所示逻辑的两种方法以及各自的弊端。第一种方式是:
int t = p(x);System.out.println( r(q1(t), q2(t)) );
这段代码看起来很清晰,不过Java会顺次执行对q1和q2的调用,而这是你希望避免的,因为你的目标是要充分利用硬件的并行处理能力。
另一种方法是使用Future并行地执行方法f和g:
int t = p(x);Future<Integer> a1 = executorService.submit(() -> q1(t));Future<Integer> a2 = executorService.submit(() -> q2(t));System.out.println( r(a1.get(),a2.get()));
注意,由于“线框–管道”图的形状,这个例子中并未用Future封装p和r。p需要在其他所有任务之前完成,而r需要在其他所有任务之后执行。如果修改一下这个例子,用下面的代码去模拟,刚才的那几个条件就都不是问题了:
System.out.println( r(q1(t), q2(t)) + s(x) );
这段代码中,我们需要用Future封装五个使用的函数(p、q1、q2、r和s)才能获得最大程度的并发。
这一方案在系统并发度不大的情况下工作得很好。但如果系统变得越来越大,带有很多相互独立的“线框–管道”图,甚至有些线框内部还使用了自己的线框和管道会怎样呢?15.1.2节中讨论过,这种情况下,大量的任务(由于调用了get()方法)会处于等待Future结束的状态,导致最终无法充分发挥硬件的并发处理能力,甚至出现死锁。此外,要深入理解这么大规模系统的结构才能确定多少任务容易由于执行get()处于等待状态,而这是非常困难的。Java 8的解决方案是使用结合器,细节请参考15.4节的CompletableFuture。你已经知道我们可以使用compose()和andThen()这样的方法将两个方法合成一个新的方法(详情请参考第3章)。假设方法add1的功能是将l和一个整型数相加,而dble可以倍增一个整型数,那么你可以编写下面的代码,创建一个函数对它的参数执行倍增操作,并将计算结果与l求和返回:
Function<Integer, Integer> myfun = add1.andThen(dble);
不过“线框–管道”图也可以直接使用结合器实现,效果同样不错。图15-7可以借助Java的Function p、q1、q2以及BiFunction r简洁地表示如下:
p.thenBoth(q1,q2).thenCombine(r)
遗憾的是,无论是thenBoth还是thenCombine,其形式都不属于Java的Function或BiFunction类。
下一节会学习类似的结合器思想是如何在CompletableFuture中工作的,避免任务使用get()时发生等待。
结束本节内容之前,我们想再次强调,“线框–管道”模型可以帮助你梳理思路和代码。某种程度上,它提升了构建大型系统的抽象层次。你通过画线框(或者在你的程序中使用结合器)表达你希望执行的计算,接着该线框被执行,这种方式比你直接手写计算任务可能高效不少。结合器不仅适合数学计算,也适合Future和反应式数据流。15.5节会对“线框–管道”图进行归纳,并引入“弹珠图”(marble diagram)。弹珠图的每个管道中可能有多个弹珠(代表消息)。“线框–管道”模型还能帮你切换视角,从直接通过编程处理并发到利用结合器由它们内部执行这些工作。类似地,Java 8的流也改变了我们处理数据的视角,程序员现在不需要迭代遍历数据结构了,这部分工作可以交由结合器在流的内部完成。
