有没有一种方法可以部分控制Java并行流的顺序?

时间:2019-04-29 15:32:59

标签: java java-stream

我知道尝试使并行流按特定顺序执行每个元素没有任何意义。由于它并行运行数据,因此在排序中显然会存在一些不确定性。但是,我想知道是否有可能使它按顺序执行“排序”,或者至少尝试使该顺序与顺序执行时的顺序类似。

用例

我需要对几个数组的每个值组合执行一些代码。我创建了所有可能的索引组合的流,如下所示(为了不泄露专有信息,对变量名进行了混淆,我保证通常不会为变量arr1arr2等命名) ):

public static void doMyComputation(double[] arr1, double[] arr2, double[] arr3) {
  DoubleStream.of(arr1).mapToObj(Double::valueOf)
    .flatMap(
      i1->DoubleStream.of(arr2).mapToObj(Double::valueOf)
        .flatMap(
          i2->DoubleStream.of(arr3).mapToObj(Double::valueOf)
            .flatMap(
              i3->new Inputs(i1,i2,i3)
             )
        )
    )
    .parallel()
    .forEach(input -> doComputationallyIntensiveThing(input.i1, input.i2, input.i3);

这很好用(或者至少是真实版本,我简化了我在此处发布的代码片段的一些操作,因此我可能弄乱了代码片段)。我希望由于并行性,我不会看到依次按arr1[0], arr2[0], arr3[0],随后依次为arr1[0], arr2[0], arr3[1]等的值。但是,我希望至少可以看到带有首先从arr1开始的几个值,然后慢慢地进行到arr1的结尾。令我惊讶的是,它甚至还没有达到这个水平。

问题在于,在该doComputationallyIntensiveThing方法中,只有当我们从arr1中看到许多相同的值时,某些缓存才表现良好。如果将这些值完全随机地输入,则缓存带来的弊大于利。

是否有任何方法可以提示流以按arr1中的值将输入分组在一起的顺序执行输入?

如果没有,那么我可以为arr1中的每个值创建一个新流,并且可以正常工作,但是我想看看是否有一种方法可以在一个流中完成所有操作

1 个答案:

答案 0 :(得分:1)

通常,您不应对并行流假定特定的处理顺序,但假设您的算法是正确的,无论实际的处理顺序如何,都可以推断顺序与性能之间的关系。

Stream实现 已经设计为允许从处理连续元素中受益-对于本地处理器。因此,当您拥有数百个元素的流时,为简化起见说IntStream.range(0, 100),并使用四个其他空闲的CPU内核对其进行处理,则实现会将其分为四个范围0-25、25-50、50-75最好在75-100之间进行独立处理。因此,每个处理器将在本地处理连续的元素,并受益于底层效果,例如一次将多个数组元素提取到其本地缓存中,等等。

因此,您的doComputationallyIntensiveThing方法的问题似乎在于,缓存(和您的监视)不在本地工作。因此,与上述示例相同,该操作将从同时执行0255075的并行操作开始,如果它们在经过相似的时间后完成,随后将并行评估1265176。如果第一次评估的四个元素中的任何一个“获胜”并确定了缓存的数据,则它将仅适用于接下来的四个值中的一个。如果线程的时间发生变化,该比率将变得更糟。

一种解决方案是更改doComputationallyIntensiveThing以使用线程本地缓存,从而受益于每个线程中连续元素的处理。然后,您定义Stream操作的方式非常适合此操作,因为它可以反复看到arr1的相同元素。不过,您可以简化代码并消除大量装箱开销:

Arrays.stream(arr1).parallel().forEach(i1 ->
    Arrays.stream(arr2).forEach(i2 ->
        Arrays.stream(arr3).forEach(i3 ->
            doComputationallyIntensiveThing(i1, i2, i3))));

但是,由于并行Stream在控件之外使用线程池,因此这带来了随后清理线程本地缓存的挑战。

目前,该方法更简单的解决方法是更改​​嵌套:

Arrays.stream(arr2).parallel().forEach(i2 ->
    Arrays.stream(arr1).forEach(i1 ->
        Arrays.stream(arr3).forEach(i3 ->
            doComputationallyIntensiveThing(i1, i2, i3))));

现在,arr2以上述方式分裂。然后,每个工作线程将在arr1上执行相同的迭代,处理它的每个元素的次数与arr3中的元素一样多。这样可以利用线程间缓存行为,但是存在由于时序差异而导致线程不同步的风险,最终导致与以前相同的情况。

更好的选择是重新设计doComputationallyIntensiveThing,创建两种不同的方法,一种为arr1的特定元素准备操作,然后返回包含该元素缓存数据的对象,而另一种方法为利用缓存的数据进行实际处理:

Arrays.stream(arr1).parallel()
    .mapToObj(i1 -> prepareOperation(i1))
    .forEach(cached ->
        Arrays.stream(arr2).forEach(i2 ->
            Arrays.stream(arr3).forEach(i3 ->
                doComputationallyIntensiveThing(cached, i2, i3))));

这里,prepareOperation返回的每个实例都与arr1的特定元素相关联,并充当与其相关联的任何数据的本地缓存,但是当处理特定的数据时,通常会收集垃圾元素结束。因此,无需清理。

原则上,如果prepareOperation仅返回空的Holder对象(由对特定元素的第一次调用doComputationallyIntensiveThing来填充),这也将起作用。

相关问题