Java paralel akışı - parallel () yönteminin çağrılma sırası [kapalı]


11
AtomicInteger recordNumber = new AtomicInteger();
Files.lines(inputFile.toPath(), StandardCharsets.UTF_8)
     .map(record -> new Record(recordNumber.incrementAndGet(), record)) 
     .parallel()           
     .filter(record -> doSomeOperation())
     .findFirst()

Bunu yazdığımda, haritadan sonra paralel yerleştirildiğinden, konuların sadece harita çağrısına çıkacağını varsaydım. Ancak dosyadaki bazı satırlar her yürütme için farklı kayıt numaraları alıyordu.

Akışların başlık altında nasıl çalıştığını anlamak için resmi Java akışı belgelerini ve birkaç web sitesini okudum .

Birkaç soru:

  • Java paralel akışı dayalı çalışır SplitIterator biz bu koleksiyonların paralel akışı dışarı inşa zaman LinkedList vb ArrayList gibi her koleksiyonu ile uygulanmaktadır, karşılık gelen bölme yineleyici yineleme koleksiyonunu bölünmüş için kullanılan ve edilecektir. Bu, paralelliğin neden haritanın sonucu yerine orijinal giriş kaynağı (Dosya satırları) düzeyinde (yani Record pojo) gerçekleştiğini açıklar. Anlayışım doğru mu?

  • Benim durumumda, girdi bir dosya GÇ akışıdır. Hangi bölünmüş yineleyici kullanılacak?

  • parallel()Boru hattına nereye yerleştirdiğimiz önemli değil . Orijinal giriş kaynağı her zaman bölünecek ve geri kalan ara işlemler uygulanacaktır.

    Bu durumda, Java, kullanıcıların orijinal kaynak dışında boru hattının herhangi bir yerine paralel işlem yapmasına izin vermemelidir. Çünkü, java akışının dahili olarak nasıl çalıştığını bilmeyenler için yanlış anlayış veriyor. parallel()İşlem Stream nesne türü için tanımlanmış olacağını biliyorum ve bu nedenle, bu şekilde çalışıyor. Ancak, alternatif bir çözüm sunmak daha iyidir.

  • Yukarıdaki kod snippet'inde, giriş dosyasındaki her kayda bir satır numarası eklemeye çalışıyorum ve bu yüzden sipariş edilmelidir. Ancak, doSomeOperation()ağır ağırlık mantığı olduğundan paralel olarak uygulamak istiyorum . Ulaşmanın bir yolu kendi özelleştirilmiş bölünmüş yineleyicimi yazmaktır. Başka yolu var mı?


2
Java içerik oluşturucularının arayüzü nasıl tasarlamaya karar verdikleri ile ilgili daha fazla şey var. Taleplerinizi boru hattına koyarsınız ve nihai bir işlem olmayan her şey önce toplanır. parallel()altta yatan akış nesnesine uygulanan genel bir değiştirici isteğinden başka bir şey değildir. Boruya son işlemleri uygulamadığınızda, yani hiçbir şey "yürütülmediği sürece" yalnızca bir kaynak akışının olduğunu unutmayın. Bunu söyledikten sonra, temelde sadece Java tasarım seçeneklerini sorguluyorsunuz. Hangi fikir tabanlı ve biz gerçekten bu konuda yardımcı olamaz.
Zabuzard

1
Tamamen fikrinizi ve karışıklığınızı alıyorum ama çok daha iyi çözümler olduğunu sanmıyorum. Yöntem Streamdoğrudan arayüzde sunulur ve güzel basamaklı olması nedeniyle her işlem Streamtekrar verir . Birisinin size vermek istediğini düşünün, Streamancak zaten mapbunun gibi birkaç işlem uyguladı . Bir kullanıcı olarak, yine de paralel olarak yürütülüp yürütülmeyeceğine karar vermek istersiniz. Bu nedenle parallel(), akış zaten mevcut olsa da, yine de arama yapmanız mümkün olmalıdır .
Zabuzard

1
Buna ek olarak, neden bir akışın bir kısmını sırayla yürütmek ve daha sonra paralel olarak geçmek isteyeceğinizi sormak isterim. Akış zaten paralel yürütmeye hak kazanmak için yeterince büyükse, bu muhtemelen boru hattındaki önceki her şey için de geçerlidir. Öyleyse neden bu kısım için paralel yürütmeyi kullanmıyorsunuz? Boyutu önemli ölçüde arttırırsanız flatMapveya iş parçacığı güvenli olmayan yöntemler veya benzeri çalıştırırsanız gibi kenar durumlar olduğunu anladım .
Zabuzard

1
@Zabuza Java tasarım seçimini sorgulamıyorum ama sadece endişemi dile getiriyorum. Herhangi bir temel java akışı kullanıcısı, akışın çalışmasını anlamadıkça aynı karışıklığa sahip olabilir. Gerçi 2. yorumunuza tamamen katılıyorum. Bahsettiğiniz gibi kendi dezavantajına sahip olabilecek olası bir çözümü vurguladım. Ancak, başka bir şekilde çözülüp çözülemeyeceğini görebiliriz. 3. yorumunuzla ilgili olarak, açıklamamın son noktasında kullanım durumumdan bahsetmiştim
kaşif

1
Zaman @Eugene Pathyerel dosya sisteminde ve bir son JDK kullanıyoruz, spliterator bazı hatta karşı-üretken olabilirler 1024 Fakat dengeli bir bölme dozajları katları daha iyi paralel işlem yeteneğine sahip olacak findFirstsenaryolar ...
Holger

Yanıtlar:


8

Bu, paralelliğin neden haritanın sonucu yerine orijinal giriş kaynağı (Dosya satırları) düzeyinde (yani Record pojo) gerçekleştiğini açıklar.

Tüm akış paralel veya ardışıktır. Sıralı veya paralel olarak çalıştırılacak bir işlem alt kümesi seçmiyoruz.

Terminal işlemi başlatıldığında, akış boru hattı, çağrıldığı akışın yönüne bağlı olarak sırayla veya paralel olarak yürütülür. [...] Terminal işlemi başlatıldığında, akış boru hattı, çağrıldığı akışın moduna bağlı olarak sırayla veya paralel olarak yürütülür. aynı kaynak

Bahsettiğiniz gibi, paralel akışlar bölünmüş yineleyiciler kullanır. Açıkçası, bu, işlemler çalışmaya başlamadan önce verileri bölümlendirmektir.


Benim durumumda, girdi bir dosya GÇ akışıdır. Hangi bölünmüş yineleyici kullanılacak?

Kaynağa baktığımda, kullandığını görüyorum java.nio.file.FileChannelLinesSpliterator


Boru hattına paralel () yerleştirdiğimiz önemli değil. Orijinal giriş kaynağı her zaman bölünecek ve geri kalan ara işlemler uygulanacaktır.

Sağ. Hatta parallel()ve sequential()birden çok kez arayabilirsiniz . Son çağrılan kazanır. Aradığımızda parallel(), geri dönen akış için bunu ayarladık; ve yukarıda belirtildiği gibi, tüm işlemler sırayla veya paralel olarak gerçekleştirilir.


Bu durumda, Java, kullanıcıların orijinal kaynak dışında boru hattının herhangi bir yerine paralel işlem yapmasına izin vermemelidir ...

Bu bir fikir meselesi haline geliyor. Zabuza'nın JDK tasarımcılarının seçimini desteklemek için iyi bir neden verdiğini düşünüyorum.


Ulaşmanın bir yolu kendi özelleştirilmiş bölünmüş yineleyicimi yazmaktır. Başka yolu var mı?

Bu işlemlerinize bağlıdır

  • findFirst()Gerçek terminal işleminiz varsa , paralel yürütme konusunda endişelenmenize bile gerek yoktur, çünkü doSomething()yine de çok fazla çağrı olmayacaktır ( findFirst()kısa devre). .parallel()aslında, birden fazla elemanın işlenmesine neden olabilirken, findFirst()sıralı bir akışta bunu engelleyebilir.
  • Terminal işleminiz fazla veri oluşturmazsa, Recordnesnelerinizi sıralı bir akış kullanarak oluşturabilir ve ardından sonucu paralel olarak işleyebilirsiniz:

    List<Record> smallData = Files.lines(inputFile.toPath(), 
                                         StandardCharsets.UTF_8)
      .map(record -> new Record(recordNumber.incrementAndGet(), record)) 
      .collect(Collectors.toList())
      .parallelStream()     
      .filter(record -> doSomeOperation())
      .collect(Collectors.toList());
    
  • Eğer boru hattınız belleğe çok fazla veri yüklerse (bu nedenle kullanmanızın nedeni olabilir Files.lines()), belki de özel bir bölünmüş yineleyiciye ihtiyacınız olacaktır. Ben oraya gitmeden önce, ben, diğer seçeneklere bakmak istiyorum (başlamak için bir id sütun ile böyle tasarruf satırları - bu sadece benim görüşüm).
    Ayrıca kayıtları daha küçük gruplar halinde işlemeye çalışırım, şöyle:

    AtomicInteger recordNumber = new AtomicInteger();
    final int batchSize = 10;
    
    try(BufferedReader reader = Files.newBufferedReader(inputFile.toPath(), 
            StandardCharsets.UTF_8);) {
        Supplier<List<Record>> batchSupplier = () -> {
            List<Record> batch = new ArrayList<>();
            for (int i = 0; i < batchSize; i++) {
                String nextLine;
                try {
                    nextLine = reader.readLine();
                } catch (IOException e) {
                    //hanlde exception
                    throw new RuntimeException(e);
                }
    
                if(null == nextLine) 
                    return batch;
                batch.add(new Record(recordNumber.getAndIncrement(), nextLine));
            }
            System.out.println("next batch");
    
            return batch;
        };
    
        Stream.generate(batchSupplier)
            .takeWhile(list -> list.size() >= batchSize)
            .map(list -> list.parallelStream()
                             .filter(record -> doSomeOperation())
                             .collect(Collectors.toList()))
            .flatMap(List::stream)
            .forEach(System.out::println);
    }
    

    Bu doSomeOperation(), tüm verileri belleğe yüklemeden paralel olarak yürütülür. Ancak bunun batchSizebir düşünce verilmesi gerektiğini unutmayın .


1
Açıklama için teşekkürler. Vurguladığınız 3. çözümü bilmek iyidir. Ben takeWhile ve Tedarikçi kullanmadıysanız bir göz atacağım.
explorer

2
Özel bir Spliteratoruygulama, bundan daha karmaşık paralel işlemeye izin verirken bundan daha karmaşık olmaz ...
Holger

1
İçinizdeki her parallelStreamoperasyonların bir paralellik sınırlı olurken, operasyon başlatılması ve sonuç için bekleyen için sabit yüke sahiptir batchSize. İlk olarak, boştaki iplikleri önlemek için şu anda mevcut olan CPU çekirdeklerinin çoğuna ihtiyacınız vardır. Daha sonra, sayı sabit ek yükü telafi edecek kadar yüksek olmalıdır, ancak sayı ne kadar yüksek olursa, paralel işleme başlamadan önce gerçekleşen sıralı okuma işleminin verdiği duraklama o kadar yüksek olur.
Holger

1
Dış akımı paralel çevirmek, Stream.generateOP'nin amaçlanan kullanım durumları ile çalışmayan, sırasız bir akış oluşturan noktanın yanı sıra mevcut uygulamada iç kısımla kötü etkileşime neden olacaktır findFirst(). Buna karşılık, parçalara dönen bir ayırıcıya sahip tek bir paralel akış, trySplitdüz ileri çalışır ve işçi iş parçacıklarının , bir öncekinin tamamlanmasını beklemeden bir sonraki parçayı işlemesine izin verir.
Holger

2
Bir findFirst()operasyonun sadece az sayıda elemanı işleyeceğini varsaymak için hiçbir neden yoktur . İlk eşleşme, tüm öğelerin% 90'ı işlendikten sonra da gerçekleşebilir. Ayrıca, on milyon çizgiye sahipken,% 10'dan sonra bir eşleşme bile bir milyon çizgiyi işlemeyi gerektirir.
Holger

7

Orijinal Stream tasarımı, farklı paralel yürütme ayarlarıyla sonraki boru hattı aşamalarını destekleme fikrini içeriyordu, ancak bu fikir terk edildi. API bu zamandan kaynaklanabilir, ancak öte yandan, arayanı paralel veya sıralı yürütme için tek bir kesin karar vermeye zorlayan bir API tasarımı çok daha karmaşık olacaktır.

SpliteratorTarafından kullanılan gerçek Files.lines(…)uygulamaya bağlıdır. Java 8'de (Oracle veya OpenJDK) her zaman ile aynı olur BufferedReader.lines(). Daha yeni JDK'larda, Pathvarsayılan dosya sistemine aitse ve karakter kümesi bu özellik için desteklenenlerden biriyse, özel bir Spliteratoruygulamaya sahip bir Akış elde edersiniz java.nio.file.FileChannelLinesSpliterator. Önkoşullar karşılanmazsa, BufferedReader.lines()yine de Iteratoriçinde uygulanmış BufferedReaderve içine sarılmış olan ile aynı olur Spliterators.spliteratorUnknownSize.

Özel göreviniz en iyi Spliterator, paralel işlemeden önce kaynakta satır numaralandırmasını, kısıtlama olmaksızın sonraki paralel işlemeye izin vermek için gerçekleştirebilen bir özel ile ele alınır .

public static Stream<Record> records(Path p) throws IOException {
    LineNoSpliterator sp = new LineNoSpliterator(p);
    return StreamSupport.stream(sp, false).onClose(sp);
}

private static class LineNoSpliterator implements Spliterator<Record>, Runnable {
    int chunkSize = 100;
    SeekableByteChannel channel;
    LineNumberReader reader;

    LineNoSpliterator(Path path) throws IOException {
        channel = Files.newByteChannel(path, StandardOpenOption.READ);
        reader=new LineNumberReader(Channels.newReader(channel,StandardCharsets.UTF_8));
    }

    @Override
    public void run() {
        try(Closeable c1 = reader; Closeable c2 = channel) {}
        catch(IOException ex) { throw new UncheckedIOException(ex); }
        finally { reader = null; channel = null; }
    }

    @Override
    public boolean tryAdvance(Consumer<? super Record> action) {
        try {
            String line = reader.readLine();
            if(line == null) return false;
            action.accept(new Record(reader.getLineNumber(), line));
            return true;
        } catch (IOException ex) {
            throw new UncheckedIOException(ex);
        }
    }

    @Override
    public Spliterator<Record> trySplit() {
        Record[] chunks = new Record[chunkSize];
        int read;
        for(read = 0; read < chunks.length; read++) {
            int pos = read;
            if(!tryAdvance(r -> chunks[pos] = r)) break;
        }
        return Spliterators.spliterator(chunks, 0, read, characteristics());
    }

    @Override
    public long estimateSize() {
        try {
            return (channel.size() - channel.position()) / 60;
        } catch (IOException ex) {
            return 0;
        }
    }

    @Override
    public int characteristics() {
        return ORDERED | NONNULL | DISTINCT;
    }
}

0

Aşağıdakiler, paralel uygulamanın ne zaman uygulandığının basit bir göstergesidir. Peek çıktısı iki örnek arasındaki farkı açıkça göstermektedir. Not: mapÇağrı, daha önce başka bir yöntem eklemek için atılmıştır parallel.

IntStream.rangeClosed (1,20).peek(a->System.out.print(a+" "))
        .map(a->a + 200).sum();
System.out.println();
IntStream.rangeClosed(1,20).peek(a->System.out.print(a+" "))
        .map(a->a + 200).parallel().sum();
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.