กระแสข้อมูล Java แบบขนาน - ลำดับของการเรียกใช้เมธอด parallel () [ปิด]


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

เมื่อฉันเขียนสิ่งนี้ฉันสันนิษฐานว่าเธรดจะวางไข่เฉพาะการเรียกแผนที่เนื่องจากวางขนานหลังจากแผนที่ แต่บางบรรทัดในไฟล์ได้รับหมายเลขบันทึกที่แตกต่างกันสำหรับการดำเนินการทุกครั้ง

ฉันอ่านเอกสาร Java streamอย่างเป็นทางการและเว็บไซต์บางแห่งเพื่อทำความเข้าใจวิธีการทำงานของสตรีมภายใต้ฮูด

คำถามเล็กน้อย:

  • Java กระแสขนานทำงานบนพื้นฐานของSplitIteratorซึ่งดำเนินการโดยคอลเลกชันทุกอย่างเช่น ArrayList, LinkedList เป็นต้นเมื่อเราสร้างสตรีมขนานจากคอลเลกชันเหล่านั้นตัววนซ้ำที่สอดคล้องกันนั้นจะถูกใช้เพื่อแยกและย้ำคอลเลกชัน สิ่งนี้อธิบายว่าเหตุใดจึงเกิดความขนานที่แหล่งอินพุตดั้งเดิม (เส้นไฟล์) แทนที่จะเป็นผลลัพธ์ของแผนที่ (เช่นเร็กคอร์ด pojo) ความเข้าใจของฉันถูกต้องหรือไม่

  • ในกรณีของฉันอินพุตเป็นไฟล์สตรีม IO จะใช้ตัววนซ้ำแบบแยกใด

  • ไม่สำคัญว่าเราจะวางไว้ที่ไหนparallel()ในท่อส่ง แหล่งอินพุตดั้งเดิมจะถูกแยกเสมอและจะใช้การดำเนินการระดับกลางที่เหลืออยู่

    ในกรณีนี้ Java ไม่ควรอนุญาตให้ผู้ใช้ทำการดำเนินการแบบขนานที่ใดก็ได้ในไปป์ไลน์ยกเว้นที่แหล่งต้นฉบับ เนื่องจากเป็นการให้ความเข้าใจที่ผิดสำหรับผู้ที่ไม่ทราบว่า java stream ทำงานอย่างไรภายใน ฉันรู้ว่าparallel()การดำเนินการจะได้รับการกำหนดไว้สำหรับประเภทวัตถุสตรีมและดังนั้นจึงทำงานได้ด้วยวิธีนี้ แต่มันจะดีกว่าที่จะให้ทางออกทางเลือกบางอย่าง

  • ในตัวอย่างโค้ดข้างต้นฉันพยายามเพิ่มหมายเลขบรรทัดในทุกเร็กคอร์ดในไฟล์อินพุตดังนั้นควรสั่งซื้อ อย่างไรก็ตามฉันต้องการนำไปใช้doSomeOperation()ควบคู่กันเพราะเป็นตรรกะที่มีน้ำหนักมาก วิธีหนึ่งในการบรรลุคือการเขียนตัวแยกซ้ำที่กำหนดเองของฉัน มีวิธีอื่น ๆ ?


2
มีอะไรเพิ่มเติมเกี่ยวกับวิธีที่ผู้สร้าง Java ตัดสินใจออกแบบอินเตอร์เฟส คุณส่งคำขอไปยังไปป์ไลน์และทุกอย่างที่ไม่ใช่การดำเนินการขั้นสุดท้ายจะถูกรวบรวมไว้ก่อน parallel()ไม่มีอะไรมากไปกว่าการร้องขอโมดิฟายเออร์ทั่วไปซึ่งใช้กับวัตถุสตรีมที่สำคัญ โปรดจำไว้ว่ามีเพียงสตรีมต้นทางหนึ่งรายการหากคุณไม่ได้ใช้การดำเนินการขั้นสุดท้ายกับไปป์นั่นคือตราบใดที่ไม่มีการ "ดำเนินการ" ต้องบอกว่าคุณแค่ตั้งคำถามเกี่ยวกับตัวเลือกการออกแบบ Java ซึ่งเป็นพื้นฐานของความคิดเห็นและเราไม่สามารถช่วยได้จริงๆ
Zabuzard

1
ฉันได้รับจุดและความสับสนของคุณโดยสิ้นเชิง แต่ฉันไม่คิดว่ามีวิธีแก้ปัญหาที่ดีกว่ามาก มีการเสนอวิธีการในStreamอินเตอร์เฟสโดยตรงและเนื่องจากการเชื่อมต่อที่ดีทุกการดำเนินการกลับมาStreamอีกครั้ง ลองนึกภาพใครบางคนที่ต้องการให้คุณStreamได้ใช้การดำเนินการสองสามอย่างเช่นmapนี้ คุณในฐานะผู้ใช้ยังคงต้องการที่จะตัดสินใจว่าจะให้มันทำงานแบบขนานหรือไม่ ดังนั้นจึงเป็นไปได้ที่คุณจะparallel()ยังคงโทรถึงแม้ว่าสตรีมจะมีอยู่แล้ว
Zabuzard

1
นอกจากนี้ฉันอยากถามว่าทำไมคุณถึงต้องการเรียกใช้ส่วนหนึ่งของกระแสข้อมูลตามลำดับจากนั้นสลับเป็นขนานในภายหลัง หากกระแสมีขนาดใหญ่พอที่จะมีคุณสมบัติสำหรับการดำเนินการแบบขนานนี่อาจใช้กับทุกอย่างก่อนหน้านี้ในไปป์ไลน์ เหตุใดจึงไม่ใช้การประมวลผลแบบขนานสำหรับส่วนนั้นด้วย ฉันได้รับว่ามีกรณีขอบเช่นถ้าคุณเพิ่มขนาดอย่างมากด้วยflatMapหรือถ้าคุณดำเนินการวิธีการเธรดที่ไม่ปลอดภัยหรือคล้ายกัน
Zabuzard

1
@Zabuza ฉันไม่ได้ถามตัวเลือกการออกแบบ java แต่ฉันแค่เพิ่มความกังวลของฉัน ผู้ใช้ java stream พื้นฐานสามารถสับสนได้เหมือนกันเว้นแต่พวกเขาจะเข้าใจการทำงานของ stream ฉันเห็นด้วยกับความคิดเห็นที่ 2 ของคุณทั้งหมด ฉันเพิ่งเน้นโซลูชันที่เป็นไปได้วิธีหนึ่งซึ่งอาจมีข้อเสียของตัวเองตามที่คุณกล่าวถึง แต่เราสามารถดูว่าสามารถแก้ไขได้ด้วยวิธีอื่นหรือไม่ เกี่ยวกับความคิดเห็นที่ 3 ของคุณฉันได้กล่าวถึงกรณีใช้งานของฉันในจุดสุดท้ายของคำอธิบายของฉันแล้ว
นักสำรวจ

1
@Eugene เมื่อPathอยู่ในระบบไฟล์ในเครื่องและคุณกำลังใช้ JDK เมื่อเร็ว ๆ นี้ตัวแยกสัญญาณจะมีความสามารถในการประมวลผลแบบขนานที่ดีกว่าการสร้างชุดข้อมูลทวีคูณที่ 1024 แต่การแบ่งแบบสมดุลอาจจะตอบโต้ได้ในบางfindFirstสถานการณ์ ...
Holger

คำตอบ:


8

สิ่งนี้อธิบายว่าเหตุใดจึงเกิดความขนานที่แหล่งอินพุตดั้งเดิม (เส้นไฟล์) แทนที่จะเป็นผลลัพธ์ของแผนที่ (เช่นเร็กคอร์ด pojo)

สตรีมทั้งหมดเป็นแบบขนานหรือเรียงตามลำดับ เราไม่ได้เลือกชุดย่อยของการดำเนินงานเพื่อเรียกใช้ตามลำดับหรือขนาน

เมื่อการดำเนินการของเทอร์มินัลเริ่มต้นไปป์ไลน์จะถูกดำเนินการตามลำดับหรือขนานขึ้นอยู่กับการวางแนวของกระแสที่ถูกเรียก [... ] เมื่อการดำเนินการของเทอร์มินัลเริ่มต้นไปป์ไลน์จะถูกดำเนินการตามลำดับหรือขนานขึ้นอยู่กับโหมดของกระแสที่มันถูกเรียก แหล่งเดียวกัน

ในขณะที่คุณพูดถึงสตรีมแบบขนานจะใช้ตัววนซ้ำแบบแยก ชัดเจนนี่คือการแบ่งพาร์ติชันข้อมูลก่อนที่การดำเนินการจะเริ่มทำงาน


ในกรณีของฉันอินพุตเป็นไฟล์สตรีม IO จะใช้ตัววนซ้ำแบบแยกใด

เมื่อมองไปที่แหล่งข้อมูลฉันเห็นว่ามันใช้ java.nio.file.FileChannelLinesSpliterator


ไม่สำคัญว่าเราจะวางขนาน () ในท่อส่งที่ใด แหล่งอินพุตดั้งเดิมจะถูกแยกเสมอและจะใช้การดำเนินการระดับกลางที่เหลืออยู่

ขวา. คุณยังสามารถโทรparallel()และsequential()หลายครั้ง คนที่ถูกเรียกใช้ครั้งสุดท้ายจะเป็นผู้ชนะ เมื่อเราเรียกparallel()เราตั้งค่านั้นสำหรับกระแสที่ส่งคืน และตามที่ระบุไว้ข้างต้นการดำเนินการทั้งหมดจะทำงานตามลำดับหรือขนาน


ในกรณีนี้ Java ไม่ควรอนุญาตให้ผู้ใช้ทำการดำเนินการแบบขนานที่ใดก็ได้ในไปป์ไลน์ยกเว้นที่แหล่งดั้งเดิม ...

เรื่องนี้กลายเป็นเรื่องของความคิดเห็น ฉันคิดว่า Zabuza ให้เหตุผลที่ดีในการสนับสนุนทางเลือกของนักออกแบบ JDK


วิธีหนึ่งในการบรรลุคือการเขียนตัวแยกซ้ำที่กำหนดเองของฉัน มีวิธีอื่น ๆ ?

ขึ้นอยู่กับการปฏิบัติการของคุณ

  • หากfindFirst()เป็นการใช้งานเทอร์มินัลที่แท้จริงของคุณคุณไม่จำเป็นต้องกังวลเกี่ยวกับการดำเนินการแบบขนานเพราะจะไม่มีการเรียกจำนวนมากdoSomething()ไป ( findFirst()การลัดวงจร) .parallel()ในความเป็นจริงอาจทำให้องค์ประกอบมากกว่าหนึ่งองค์ประกอบถูกประมวลผลในขณะfindFirst()ที่กระแสข้อมูลแบบต่อเนื่องจะป้องกันไม่ให้
  • หากการทำงานของเทอร์มินัลไม่ได้สร้างข้อมูลมากนักบางทีคุณสามารถสร้างRecordวัตถุของคุณโดยใช้การสตรีมแบบต่อเนื่องจากนั้นประมวลผลผลลัพธ์แบบขนาน:

    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());
    
  • หากไพพ์ไลน์ของคุณจะโหลดข้อมูลจำนวนมากในหน่วยความจำ (ซึ่งอาจเป็นสาเหตุที่คุณใช้Files.lines()) บางทีคุณอาจต้องการตัววนซ้ำแบบกำหนดเอง ก่อนที่ฉันจะไปที่นั่นฉันจะดูตัวเลือกอื่น ๆ (เช่นบรรทัดการบันทึกที่มีคอลัมน์ id เพื่อเริ่มต้นด้วย - นั่นเป็นเพียงความคิดเห็นของฉัน)
    ฉันจะพยายามประมวลผลบันทึกเป็นชุดเล็ก ๆ เช่นนี้

    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);
    }
    

    สิ่งนี้ดำเนินการdoSomeOperation()แบบขนานโดยไม่โหลดข้อมูลทั้งหมดลงในหน่วยความจำ แต่ทราบว่าbatchSizeจะต้องได้รับความคิด


1
ขอขอบคุณสำหรับการชี้แจง. เป็นการดีที่จะทราบเกี่ยวกับโซลูชันที่ 3 ที่คุณเน้นไว้ ฉันจะดูที่ฉันไม่ได้ใช้ takeWhile และซัพพลายเออร์
นักสำรวจ

2
กำหนดเองSpliteratorการดำเนินงานจะไม่ได้รับความซับซ้อนมากขึ้นกว่านี้ขณะที่ช่วยให้การประมวลผลแบบขนานมีประสิทธิภาพมากขึ้น ...
โฮล

1
แต่ละด้านของparallelStreamการดำเนินงานที่มีค่าใช้จ่ายคงที่สำหรับการเริ่มต้นการดำเนินการและรอให้สิ้นผลในขณะที่ถูก จำกัด batchSizeให้ความเท่าเทียมของ ก่อนอื่นคุณต้องมีจำนวนแกน CPU ที่มีอยู่ในปัจจุบันเพื่อหลีกเลี่ยงเธรดที่ไม่ได้ทำงาน จากนั้นจำนวนที่ควรจะสูงพอที่จะชดเชยค่าใช้จ่ายคงที่ แต่จำนวนที่สูงกว่ายิ่งหยุดชั่วคราวที่กำหนดโดยการดำเนินการอ่านตามลำดับที่เกิดขึ้นก่อนที่การประมวลผลแบบขนานจะเริ่มขึ้น
Holger

1
เปิดขนานด้านนอกกระแสจะทำให้เกิดการรบกวนที่ไม่ดีกับด้านในการดำเนินงานในปัจจุบันนอกเหนือจากจุดที่Stream.generateก่อให้เกิดกระแสเรียงลำดับซึ่งไม่ได้ทำงานกับ OP findFirst()ของกรณีการใช้งานที่ตั้งใจไว้เช่น ในทางตรงกันข้ามกระแสข้อมูลแบบขนานเดียวกับตัวแยกสัญญาณซึ่งส่งคืนชิ้นtrySplitงานตรงไปตรงมาและอนุญาตให้เธรดผู้ปฏิบัติงานประมวลผลชิ้นถัดไปโดยไม่ต้องรอให้เสร็จก่อนหน้านี้
โฮล

2
ไม่มีเหตุผลที่จะถือว่าการfindFirst()ดำเนินการจะประมวลผลองค์ประกอบจำนวนเล็กน้อยเท่านั้น นัดแรกอาจยังคงเกิดขึ้นหลังจากประมวลผล 90% ขององค์ประกอบทั้งหมด นอกจากนี้เมื่อมีสิบล้านบรรทัดแม้จะพบการแข่งขันหลังจาก 10% ยังคงต้องการการประมวลผลหนึ่งล้านบรรทัด
โฮล

7

การออกแบบสตรีมดั้งเดิมรวมถึงแนวคิดเพื่อสนับสนุนขั้นตอนการถัดไปที่มีการตั้งค่าการดำเนินการแบบขนานที่แตกต่างกัน แต่ความคิดนี้ถูกทอดทิ้ง API อาจเกิดขึ้นจากเวลานี้ แต่ในทางกลับกันการออกแบบ API ที่บังคับให้ผู้โทรตัดสินใจอย่างชัดเจนเพียงครั้งเดียวสำหรับการดำเนินการแบบขนานหรือต่อเนื่องจะมีความซับซ้อนมากขึ้น

การSpliteratorใช้งานจริงโดยFiles.lines(…)ขึ้นอยู่กับการนำไปใช้งาน ใน Java 8 (Oracle หรือ OpenJDK) คุณจะได้รับเหมือนกันBufferedReader.lines()เสมอ ใน JDKs เมื่อเร็ว ๆ นี้ถ้าPathเป็นระบบแฟ้มเริ่มต้นและ charset เป็นหนึ่งในการสนับสนุนสำหรับคุณลักษณะนี้คุณจะได้รับกระแสด้วยการทุ่มเทการดำเนินการSpliterator java.nio.file.FileChannelLinesSpliteratorหากปัจจัยพื้นฐานจะไม่ได้พบคุณจะได้รับเช่นเดียวกับที่มีBufferedReader.lines()ซึ่งยังคงอยู่บนพื้นฐานของIteratorการดำเนินการภายในและห่อผ่านBufferedReaderSpliterators.spliteratorUnknownSize

งานเฉพาะของคุณได้รับการจัดการที่ดีที่สุดด้วยการกำหนดเองSpliteratorซึ่งสามารถดำเนินการกับหมายเลขบรรทัดที่แหล่งที่มาก่อนการประมวลผลแบบขนานเพื่อให้การประมวลผลแบบขนานที่ตามมาไม่มีข้อ จำกัด

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

และต่อไปนี้เป็นการสาธิตอย่างง่ายว่าเมื่อใช้แอปพลิเคชันแบบขนาน ผลลัพธ์จากการแอบดูอย่างชัดเจนแสดงความแตกต่างระหว่างสองตัวอย่าง หมายเหตุ: การmapโทรนั้นเพิ่งถูกโยนเพื่อเพิ่มวิธีอื่นก่อนหน้า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();
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.