簡體   English   中英

Stream.parallel() 不會更新 spliterator 的特性嗎?

[英]Doesn't Stream.parallel() update the characteristics of spliterator?

這個問題是基於這個問題的答案Stream.of 和 IntStream.range 有什么區別?

由於IntStream.range生成已排序的 stream,因此 output 到以下代碼只會生成 output 為0

IntStream.range(0, 4)
         .peek(e -> System.out.println(e))
         .sorted()
         .findFirst();

拆分器也將具有SORTED特征。 下面的代碼返回true

System.out.println(
    IntStream.range(0, 4)
             .spliterator()
             .hasCharacteristics(Spliterator.SORTED)
);

現在,如果我在第一個代碼中引入一個parallel() ,那么正如預期的那樣,output 將包含從03的所有 4 個數字,但順序是隨機的,因為 stream 由於parallel()而不再排序。

IntStream.range(0, 4)
         .parallel()
         .peek(e -> System.out.println(e))
         .sorted()
         .findFirst();

這將以任何順序產生如下所示的內容:

2
0
1
3

所以,我希望SORTED屬性由於parallel()而被刪除。 但是,下面的代碼也返回true

System.out.println(
    IntStream.range(0, 4)
             .parallel()
             .spliterator()
             .hasCharacteristics(Spliterator.SORTED)
);

為什么parallel()不改變SORTED屬性? 並且由於打印了所有四個數字, Java 如何意識到SORTED未排序,即使 SORTED 屬性仍然存在?

究竟如何做到這一點在很大程度上是一個實現細節。 您必須深入挖掘源代碼才能真正了解原因。 基本上,並行和順序流水線的處理方式不同。 查看AbstractPipeline.evaluate ,它檢查isParallel() ,然后根據管道是否並行執行不同的操作。

    return isParallel()
           ? terminalOp.evaluateParallel(this, sourceSpliterator(terminalOp.getOpFlags()))
           : terminalOp.evaluateSequential(this, sourceSpliterator(terminalOp.getOpFlags()));

如果您再查看SortedOps.OfInt ,您會發現它覆蓋了兩個方法:

@Override
public Sink<Integer> opWrapSink(int flags, Sink sink) {
    Objects.requireNonNull(sink);

    if (StreamOpFlag.SORTED.isKnown(flags))
        return sink;
    else if (StreamOpFlag.SIZED.isKnown(flags))
        return new SizedIntSortingSink(sink);
    else
        return new IntSortingSink(sink);
}

@Override
public <P_IN> Node<Integer> opEvaluateParallel(PipelineHelper<Integer> helper,
                                               Spliterator<P_IN> spliterator,
                                               IntFunction<Integer[]> generator) {
    if (StreamOpFlag.SORTED.isKnown(helper.getStreamAndOpFlags())) {
        return helper.evaluate(spliterator, false, generator);
    }
    else {
        Node.OfInt n = (Node.OfInt) helper.evaluate(spliterator, true, generator);

        int[] content = n.asPrimitiveArray();
        Arrays.parallelSort(content);

        return Nodes.node(content);
    }
}

如果它是順序管道,最終將調用opWrapSink ,而當它是並行 stream 時,將調用opEvaluateParallel (顧名思義)。 請注意,如果管道已經排序(只是將其原封不動地返回), opWrapSink不會對給定的接收器執行任何操作,但opEvaluateParallel始終評估拆分器。

另請注意,並行性和排序性並不相互排斥。 您可以擁有具有這些特性的任意組合的 stream。

“排序”是Spliterator的一個特征。 從技術上講,這不是Stream的特征(就像“並行”一樣)。 當然, parallel可以創建一個 stream 和一個全新的分離器(從原始分離器中獲取元素)和全新的特性,但是當你可以重復使用相同的分離器時,為什么要這樣做呢? 我想你在任何情況下都必須以不同的方式處理並行和順序流。

考慮到ForkJoinPool用於並行流並且它的工作原理基於工作竊取,您需要退后一步,想想一般如何解決這樣的問題。 如果您也知道Spliterator的工作原理,那將非常有幫助。 這里有一些細節。

你有一個 Stream,你將它“拆分”(非常簡化)成更小的部分,並將所有這些部分交給ForkJoinPool執行。 所有這些部分都是由單獨的線程獨立處理的。 由於我們在這里討論線程,顯然沒有事件順序,事情是隨機發生的(這就是你看到隨機順序輸出的原因)。

如果您的 stream 保留訂單,終端操作也應該保留它。 因此,雖然中間操作以任何順序執行,但您的終端操作(如果 stream 到該點是有序的)將以有序的方式處理元素。 稍微簡化一下:

System.out.println(
    IntStream.of(1,2,3)
             .parallel()
             .map(x -> {System.out.println(x * 2); return x * 2;})
             .boxed()
             .collect(Collectors.toList()));

map將以未知的順序處理元素( ForkJoinPool和線程,記住這一點),但collect將按“從左到右”的順序接收元素。


現在,如果我們將其推斷到您的示例:當您調用parallel時,stream 被分成小塊並進行處理。 例如,看看這是如何拆分的(一次)。

Spliterator<Integer> spliterator =
IntStream.of(5, 4, 3, 2, 1, 5, 6, 7, 8)
         .parallel()
         .boxed()
         .sorted()
         .spliterator()
         .trySplit(); // trySplit is invoked internally on parallel

spliterator.forEachRemaining(System.out::println);

在我的機器上打印1,2,3,4 這意味着內部實現將 stream 拆分為兩個Spliteratorleftright left[1, 2, 3, 4] ,右邊有[5, 6, 7, 8] 但事實並非如此,這些Spliterator還可以進一步拆分。 例如:

Spliterator<Integer> spliterator =
IntStream.of(5, 4, 3, 2, 1, 5, 6, 7, 8)
         .parallel()
         .boxed()
         .sorted()
         .spliterator()
         .trySplit()
         .trySplit()
         .trySplit();

spliterator.forEachRemaining(System.out::println);

如果您嘗試再次調用trySplit ,您將得到null - 意思就是,就是這樣,我不能再拆分了。

因此,您的 Stream: IntStream.range(0, 4)將被拆分為 4 個拆分器。 所有的工作都是由一個線程單獨完成的。 如果你的第一個線程知道它當前工作的這個Spliterator是“最左邊的”,就是這樣。 線程的 rest 甚至不需要啟動它們的工作 - 結果是已知的。

另一方面,這個具有“最左邊”元素的Spliterator可能只在最后啟動。 因此,前三個可能已經完成了他們的工作(因此在您的示例中調用了peek ),但它們不會“產生”所需的結果。

事實上,這是在內部完成的。 您不需要了解代碼 - 但流程和方法名稱應該是顯而易見的。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM