我知道尝试使并行流按特定顺序执行每个元素没有任何意义。由于它并行运行数据,因此在排序中显然会存在一些不确定性。但是,我想知道是否有可能使它按顺序执行“排序”,或者至少尝试使该顺序与顺序执行时的顺序类似。
我需要对几个数组的每个值组合执行一些代码。我创建了所有可能的索引组合的流,如下所示(为了不泄露专有信息,对变量名进行了混淆,我保证通常不会为变量arr1
,arr2
等命名) ):
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
中的每个值创建一个新流,并且可以正常工作,但是我想看看是否有一种方法可以在一个流中完成所有操作
答案 0 :(得分:1)
通常,您不应对并行流假定特定的处理顺序,但假设您的算法是正确的,无论实际的处理顺序如何,都可以推断顺序与性能之间的关系。
Stream实现 已经设计为允许从处理连续元素中受益-对于本地处理器。因此,当您拥有数百个元素的流时,为简化起见说IntStream.range(0, 100)
,并使用四个其他空闲的CPU内核对其进行处理,则实现会将其分为四个范围0-25、25-50、50-75最好在75-100之间进行独立处理。因此,每个处理器将在本地处理连续的元素,并受益于底层效果,例如一次将多个数组元素提取到其本地缓存中,等等。
因此,您的doComputationallyIntensiveThing
方法的问题似乎在于,缓存(和您的监视)不在本地工作。因此,与上述示例相同,该操作将从同时执行0
,25
,50
和75
的并行操作开始,如果它们在经过相似的时间后完成,随后将并行评估1
,26
,51
和76
。如果第一次评估的四个元素中的任何一个“获胜”并确定了缓存的数据,则它将仅适用于接下来的四个值中的一个。如果线程的时间发生变化,该比率将变得更糟。
一种解决方案是更改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
来填充),这也将起作用。