Paralel Sonsuz Java Akışları Bellek Yetersiz


16

Aşağıdaki Java programının neden verdiğini anlamaya çalışıyorum OutOfMemoryError, karşılık .parallel()gelmeyen program yok.

System.out.println(Stream
    .iterate(1, i -> i+1)
    .parallel()
    .flatMap(n -> Stream.iterate(n, i -> i+n))
    .mapToInt(Integer::intValue)
    .limit(100_000_000)
    .sum()
);

İki sorum var:

  1. Bu programın amaçlanan çıktısı nedir?

    Olmadan .parallel()öyle görünüyor ki bu basitçe çıkışları sum(1+2+3+...)sadece mantıklı flatMap ilk akışının en "takılıp alır" anlamına gelir söyledi.

    Paralel olarak, beklenen bir davranış olup olmadığını bilmiyorum, ama tahminim, bir şekilde ilk nya da çok akışları bir araya getirdi n, paralel işçi sayısı nerede . Parçalama / tamponlama davranışına bağlı olarak biraz farklı olabilir.

  2. Bellek yetersiz kalmasına ne sebep olur? Özellikle bu akışların başlık altında nasıl uygulandığını anlamaya çalışıyorum.

    Sanırım bir şey akışı engeller, bu yüzden asla bitmez ve üretilen değerlerden kurtulabilir, ancak şeylerin hangi sırayla değerlendirildiğini ve tamponlamanın nerede gerçekleştiğini bilmiyorum.

Düzenleme: İlgili olması durumunda, Java 11 kullanıyorum.

Editt 2: Görünüşe göre aynı şey basit program için bile olur IntStream.iterate(1,i->i+1).limit(1000_000_000).parallel().sum(), bu yüzden limityerine tembellik ile ilgili olabilir flatMap.


parallel () dahili olarak ForkJoinPool kullanır. Sanırım ForkJoin Framework, Java 7'den Java'da
aravind

Yanıtlar:


9

Ama şeylerin hangi sırayla değerlendirildiğini ve tamponlamanın nerede gerçekleştiğini ” tam olarak bilmiyorum , yani tam olarak paralel akışların konusu budur. Değerlendirme sırası belirtilmemiştir.

Örneğinizin kritik bir yönü .limit(100_000_000). Bu, uygulamanın yalnızca rasgele değerleri özetleyemeyeceği, ancak ilk 100.000.000 sayıyı özetlemesi gerektiği anlamına gelir . Referans uygulamada, .unordered().limit(100_000_000)sonucu değiştirmediğine dikkat edin, bu da sırasız vaka için özel bir uygulama olmadığını gösterir, ancak bu bir uygulama detayıdır.

Şimdi, işçi iş parçacıkları öğeleri işlediğinde, hangi iş öğelerini tüketmelerine izin verildiğini bilmek zorunda oldukları için, bu öğeleri özetleyemezler; Bu akış boyutları bilmediğinden, bu yalnızca önek öğeleri işlendiğinde bilinebilir, bu da sonsuz akışlar için asla olmaz. Böylece işçi iş parçacıkları şu an ara belleğe alınmaya devam ediyor, bu bilgi elde ediliyor.

Prensip olarak, bir işçi iş parçacığı en soldaki iş parçasını işlediğini bildiğinde, öğeleri hemen toplayabilir, sayabilir ve sınıra ulaştığında sonu bildirebilir. Böylece Akış sona erebilir, ancak bu birçok faktöre bağlıdır.

Sizin durumunuzda, makul bir senaryo, diğer çalışan iş parçacıklarının arabellekleri ayırmada en soldaki işin saydığından daha hızlı olmasıdır. Bu senaryoda, zamanlamadaki küçük değişiklikler akışın zaman zaman bir değerle dönmesini sağlayabilir.

En soldaki parçayı işleyen dışındaki tüm çalışan iş parçacıklarını yavaşlattığımızda, akışın sonlandırılmasını sağlayabiliriz (en azından çoğu çalışmada):

System.out.println(IntStream
    .iterate(1, i -> i+1)
    .parallel()
    .peek(i -> { if(i != 1) LockSupport.parkNanos(1_000_000_000); })
    .flatMap(n -> IntStream.iterate(n, i -> i+n))
    .limit(100_000_000)
    .sum()
);

¹ Takip ettiğim Stuart Marks tarafından bir öneri kullanımına soldan sağa sırayla karşılaşma sipariş ziyade işleme sırası bahsederken.


Çok güzel bir cevap! Hatta tüm iş parçacıklarının flatMap işlemlerini çalıştırmaya başlama riski vardır ve hiçbiri aslında arabellekleri (toplama) boşaltmak için ayrılmış olsun? Gerçek kullanım durumumda, sonsuz akışlar bunun yerine bellekte tutulamayacak kadar büyük dosyalardır. Bellek kullanımını azaltmak için akışı nasıl yeniden yazabilirim acaba?
Thomas Ahle

1
Kullanıyor musunuz Files.lines(…)? Java 9'da önemli ölçüde geliştirildi
Holger

1
Java 8'de yaptığı şey budur. Daha yeni JRE'lerde, yine BufferedReader.lines()de belirli durumlarda (varsayılan dosya sistemi, özel bir karakter kümesi veya daha büyük boyutta değil Integer.MAX_FILES) geri düşecektir . Bunlardan biri geçerliyse, özel bir çözüm yardımcı olabilir. Bu yeni bir soru-cevap değerinde olacaktır…
Holger

1
Integer.MAX_VALUE, tabii ki…
Holger

1
Dış akış, dosya akışı nedir? Tahmin edilebilir bir boyutu var mı?
Holger

5

En iyi tahminim, parallel()iç davranışının daha önce tembel olarak değerlendirilmiş problemleriflatMap() olan değişiklikler eklemektir .

OutOfMemoryErrorEğer bildirildi alıyorsanız hata [JDK-8202307] Bir java.lang.OutOfMemoryError alınıyor. Stream.iterator çağrılırken Java yığın alanı () flatMap sonsuz / çok büyük Akışı kullanan bir akışta () sonraki . Bilete bakarsanız, aşağı yukarı aynı yığın izini alırsınız. Bilet, aşağıdaki nedenden dolayı Won't Fix olarak kapatıldı:

iterator()Ve spliterator()diğer işlemleri kullanmak mümkün olmadığı zamanlarda yöntemleri kullanılacak "kaçış kapakları" dir. Bazı sınırlamaları vardır çünkü akış uygulamasının bir itme modelini bir çekme modeline dönüştürürler. Böyle bir geçiş , bir elemanın iki veya daha fazla elemanla (düz) eşlenmesi gibi belirli durumlarda tamponlama gerektirir . Akış uygulamalarını, muhtemelen yaygın durumlar pahasına, kaç tane öğenin iç içe eleman üretim katmanlarından çekileceğini bildirmek için bir geri basınç kavramını desteklemek önemli ölçüde karmaşıklaştıracaktır.


Bu çok ilginç! İtme / çekme geçişinin belleği tüketen tamponlama gerektirmesi mantıklıdır. Ancak benim durumumda, sadece itme kullanmanın iyi çalışması ve kalan öğeleri göründükleri gibi atması gerektiği anlaşılıyor? Ya da belki flapmap'in yineleyicinin oluşturulmasına neden olduğunu söylüyorsunuz?
Thomas Ahle

3

Gel kaynaklanır değil akışı olan sonsuzun tarafından değil gerçeğiyle değil .

Yani, eğer yorum yaparsanız .limit(...), asla hafızadan tükenmez - ama elbette, asla sona ermez.

Ayrıldıktan sonra akış, öğelerin sayısını yalnızca her bir iş parçacığında birikirse izleyebilir (gerçek akümülatör gibi görünür Spliterators$ArraySpliterator#array).

Görünüşe göre onu çoğaltmak mümkün değil flatMap, sadece aşağıdakileri çalıştırın -Xmx128m:

    System.out.println(Stream
            .iterate(1, i -> i + 1)
            .parallel()
      //    .flatMap(n -> Stream.iterate(n, i -> i+n))
            .mapToInt(Integer::intValue)
            .limit(100_000_000)
            .sum()
    );

Ancak, yorum yaptıktan sonra limit(), dizüstü bilgisayarınızı yedeklemeye karar verene kadar iyi çalışmalıdır.

Gerçek uygulama ayrıntılarının yanı sıra, bence neler olduğunu:

İle limit, sumredüktör ilk X elemanlarının toplanmasını ister, böylece hiçbir iplik kısmi toplamlar yaymaz. Her "dilim" (iplik) öğelerini biriktirmeli ve geçirmelidir. Sınırsız, böyle bir kısıtlama yoktur, bu nedenle her "dilim", sonucu eninde sonunda yayınlayacağı varsayılarak, aldığı öğelerin kısmi toplamını hesaplar (sonsuza kadar).


Ne demek "ayrıldıktan sonra"? Sınır bir şekilde bölüyor mu?
Thomas Ahle

@ThomasAhle parallel(), ForkJoinPoolparalellik elde etmek için dahili olarak kullanır . SpliteratorHer atama çalışmaları için kullanılacak ForkJoingörev, biz "bölüm" olarak burada işin birimi denebilir sanırım.
Karol Dowbecki

Peki bu neden sadece sınırla oluyor?
Thomas Ahle

@ThomasAhle Cevabı iki sentimle düzenledim.
Costi Ciudatu

1
@ThomasAhle Integer.sum(), IntStream.sumredüktör tarafından kullanılan bir kesme noktası koydu . Sınırsız sürümün her zaman işlev gören çağrıları görürken, sınırlı sürüm hiçbir zaman OOM'dan önce çağrılamaz.
Costi Ciudatu
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.