[英]Parallel stream invoking Spliterator more times than its limit
我最近發現了一個錯誤
StreamSupport.intStream(/* a Spliterator.ofInt */, true)
.limit(20)
正在調用Spliterator.ofInt.tryAdvance
超過20次。 當我把它改成
StreamSupport.intStream(/* a Spliterator.ofInt */, true)
.sequential()
.limit(20)
問題消失了。 為什么會這樣? 當tryAdvance
有副作用時,有沒有辦法在並行流上實現嚴格限制,除了在Spliterator
構建一個? (這是為了測試一些返回無限流的方法,但是測試需要達到最終結束而沒有“X毫秒循環”構造的復雜性。)
關於limit
和trySplit
應該如何交互似乎存在根本的誤解。 應該沒有比指定limit
更多的trySplit
調用的假設是完全錯誤的。
trySplit
的目的是將源數據分成兩部分,在最好的情況下分成兩半 ,因為trySplit
應該嘗試平衡分割。 因此,如果您擁有一百萬個元素的源數據集,則成功的拆分會產生兩個源數據集,每個元素包含五十萬個元素。 這與您可能已應用於流的limit(20)
完全無關,除了我們事先知道的,如果spliterator具有SIZED|SUBSIZED
特征,我們可以刪除第二個數據集,作為請求的前 20個元素只能在上半年內找到。
很容易計算出, 在最好的情況下 ,即平衡分割,我們需要十五次分割操作,每次都丟棄上半部分,之后我們在前20個元素之間進行分割,這允許我們處理前20個元素並行的元素。
這可以很容易地證明:
class DebugSpliterator extends Spliterators.AbstractIntSpliterator {
int current, fence;
DebugSpliterator() {
this(0, 1_000_000);
}
DebugSpliterator(int start, int end) {
super(end-start, ORDERED|SIZED|SUBSIZED);
current = start;
fence = end;
}
@Override public boolean tryAdvance(IntConsumer action) {
if(current<fence) {
action.accept(current++);
return true;
}
return false;
}
@Override public OfInt trySplit() {
int mid = (current+fence)>>>1;
System.out.println("trySplit() ["+current+", "+mid+", "+fence+"]");
return mid>current? new DebugSpliterator(current, current=mid): null;
}
}
StreamSupport.stream(new DebugSpliterator(), true)
.limit(20)
.forEach(x -> {});
在我的機器上,它打印:
trySplit() [0, 500000, 1000000]
trySplit() [0, 250000, 500000]
trySplit() [0, 125000, 250000]
trySplit() [0, 62500, 125000]
trySplit() [0, 31250, 62500]
trySplit() [0, 15625, 31250]
trySplit() [0, 7812, 15625]
trySplit() [0, 3906, 7812]
trySplit() [0, 1953, 3906]
trySplit() [0, 976, 1953]
trySplit() [0, 488, 976]
trySplit() [0, 244, 488]
trySplit() [0, 122, 244]
trySplit() [0, 61, 122]
trySplit() [0, 30, 61]
trySplit() [0, 15, 30]
trySplit() [15, 22, 30]
trySplit() [15, 18, 22]
trySplit() [15, 16, 18]
trySplit() [16, 17, 18]
trySplit() [0, 7, 15]
trySplit() [18, 20, 22]
trySplit() [18, 19, 20]
trySplit() [7, 11, 15]
trySplit() [0, 3, 7]
trySplit() [3, 5, 7]
trySplit() [3, 4, 5]
trySplit() [7, 9, 11]
trySplit() [4, 4, 5]
trySplit() [9, 10, 11]
trySplit() [11, 13, 15]
trySplit() [0, 1, 3]
trySplit() [13, 14, 15]
trySplit() [7, 8, 9]
trySplit() [1, 2, 3]
trySplit() [8, 8, 9]
trySplit() [5, 6, 7]
trySplit() [14, 14, 15]
trySplit() [17, 17, 18]
trySplit() [11, 12, 13]
trySplit() [12, 12, 13]
trySplit() [2, 2, 3]
trySplit() [10, 10, 11]
trySplit() [6, 6, 7]
當然,這遠遠超過二十次分割嘗試,但完全合理,因為必須將數據集拆分,直到我們在所需目標范圍內具有能夠並行處理它的子范圍。
我們可以通過刪除導致此執行策略的元信息來強制執行不同的行為:
StreamSupport.stream(new DebugSpliterator(), true)
.filter(x -> true)
.limit(20)
.forEach(x -> {});
由於Stream API不了解謂詞的行為,因此管道會失去其SIZED
特性,導致
trySplit() [0, 500000, 1000000]
trySplit() [500000, 750000, 1000000]
trySplit() [500000, 625000, 750000]
trySplit() [625000, 687500, 750000]
trySplit() [625000, 656250, 687500]
trySplit() [656250, 671875, 687500]
trySplit() [0, 250000, 500000]
trySplit() [750000, 875000, 1000000]
trySplit() [250000, 375000, 500000]
trySplit() [0, 125000, 250000]
trySplit() [250000, 312500, 375000]
trySplit() [312500, 343750, 375000]
trySplit() [125000, 187500, 250000]
trySplit() [875000, 937500, 1000000]
trySplit() [375000, 437500, 500000]
trySplit() [125000, 156250, 187500]
trySplit() [250000, 281250, 312500]
trySplit() [750000, 812500, 875000]
trySplit() [281250, 296875, 312500]
trySplit() [156250, 171875, 187500]
trySplit() [437500, 468750, 500000]
trySplit() [0, 62500, 125000]
trySplit() [875000, 906250, 937500]
trySplit() [62500, 93750, 125000]
trySplit() [812500, 843750, 875000]
trySplit() [906250, 921875, 937500]
trySplit() [0, 31250, 62500]
trySplit() [31250, 46875, 62500]
trySplit() [46875, 54687, 62500]
trySplit() [54687, 58593, 62500]
trySplit() [58593, 60546, 62500]
trySplit() [60546, 61523, 62500]
trySplit() [61523, 62011, 62500]
trySplit() [62011, 62255, 62500]
這顯示了較少的trySplit
調用,但沒有改進; 查看數字顯示現在范圍在結果元素范圍之外(如果我們使用我們的知識,所有元素將通過文件管理器)被處理,更糟糕的是,結果元素的范圍完全由單個分裂器覆蓋,導致不平行對於我們的結果元素的處理,所有其他線程都是后來被刪除的處理元素。
當然,我們可以通過更改來輕松地為我們的任務實施最佳分割
int mid = (current+fence)>>>1;
至
int mid = fence>20? 20: (current+fence)>>>1;
所以
StreamSupport.stream(new DebugSpliterator(), true)
.limit(20)
.forEach(x -> {});
結果是
trySplit() [0, 20, 1000000]
trySplit() [0, 10, 20]
trySplit() [10, 15, 20]
trySplit() [10, 12, 15]
trySplit() [12, 13, 15]
trySplit() [0, 5, 10]
trySplit() [15, 17, 20]
trySplit() [5, 7, 10]
trySplit() [0, 2, 5]
trySplit() [17, 18, 20]
trySplit() [2, 3, 5]
trySplit() [5, 6, 7]
trySplit() [15, 16, 17]
trySplit() [6, 6, 7]
trySplit() [16, 16, 17]
trySplit() [0, 1, 2]
trySplit() [7, 8, 10]
trySplit() [8, 9, 10]
trySplit() [1, 1, 2]
trySplit() [3, 4, 5]
trySplit() [9, 9, 10]
trySplit() [18, 19, 20]
trySplit() [10, 11, 12]
trySplit() [13, 14, 15]
trySplit() [11, 11, 12]
trySplit() [4, 4, 5]
trySplit() [14, 14, 15]
但這不是一個通用的分裂者,但如果限制不是二十,則表現不佳。
如果我們可以將限制結合到分裂器中,或者更一般地說,將其納入流源,我們就沒有這個問題。 因此,不是list.stream().limit(x)
,而是調用list.subList(0, Math.min(x, list.size())).stream()
,而不是random.ints().limit(x)
,使用random.ints(x)
,而不是Stream.generate(generator).limit(x)
你可以使用LongStream.range(0, x).mapToObj( index -> generator.get())
或使用這個答案的工廠方法。
對於任意流源/分裂器,對於並行流,應用limit
可能非常昂貴, 甚至可以記錄 。 嗯,並且在trySplit
中有副作用trySplit
是一個壞主意。
我不認為這是一個任何方式的錯誤,但仍然是一個非常有趣的想法, tryAdvance
可能有副作用。
據我所知,當你的trySplit
單個元素批次時,這完全是可能的。
例如,您有一個數組,並且您希望將其(通過trySplit
)拆分為每個不少於4個元素的子數組部分。 在這種情況下,當你不能再拆分時(例如,你在當前的Spliterator
至少達到了4個元素),當處理開始時 - 將調用forEachRemaning
; 反過來,它會默認調用tryAdvance
在當前每個元素Spliterator
,如默認實現看出:
default void forEachRemaining(Consumer<? super T> action) {
do { } while (tryAdvance(action));
}
顯然,因為你正在並行工作 - 一旦Thread開始它的工作(讀取executing it's forEachRemaning
),它就不能再停止了 - 所以更多的元素將會命中tryAdvance
。
因此,除了將其整合到Spliterator
本身之外,我真的不認為有辦法做到這一點; 我認為這應該有效:
static class LimitingSpliterator<T> implements Spliterator<T> {
private int limit;
private final Supplier<T> generator;
private LimitingSpliterator(Supplier<T> generator, int limit) {
this.limit = limit;
this.generator = generator;
}
static <T> LimitingSpliterator<T> of(Supplier<T> supplier, int limit) {
return new LimitingSpliterator<>(supplier, limit);
}
@Override
public boolean tryAdvance(final Consumer<? super T> consumer) {
Objects.requireNonNull(consumer);
if (limit > 0) {
--limit;
generator.get();
consumer.accept(generator.get());
return true;
}
return false;
}
@Override
public void forEachRemaining(final Consumer<? super T> consumer) {
while (limit > 0) {
consumer.accept(generator.get());
--limit;
}
}
@Override
public LimitingSpliterator<T> trySplit() {
int half = limit >> 2;
limit = limit - half;
return new LimitingSpliterator<>(generator, half);
}
@Override
public long estimateSize() {
return limit << 2;
}
@Override
public int characteristics() {
return SIZED;
}
}
對於我的用例,解決方案是使用: LongStream.range(0, streamSize).unordered().parallel().mapToInt(ignored -> nextInt())
注意:這是來自PRNG的隨機數流可能會不斷重新接種。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.