जावा समानांतर धारा - समानांतर () पद्धति को लागू करने का क्रम [बंद]


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

जब मैंने यह लिखा था तो मैंने मान लिया था कि थ्रेड्स को केवल मैप कॉल के बाद ही देखा जाएगा क्योंकि समानांतर को मैप के बाद रखा गया है। लेकिन फ़ाइल में कुछ पंक्तियों को हर निष्पादन के लिए अलग-अलग रिकॉर्ड नंबर मिल रहे थे।

मैं आधिकारिक जावा स्ट्रीम प्रलेखन और कुछ वेब साइटों को पढ़ने के लिए समझता हूं कि धाराएं हुड के नीचे कैसे काम करती हैं।

कुछ प्रश्न:

  • स्प्लिटइंटरेटर के आधार पर जावा समानांतर स्ट्रीम काम करती है , जो कि हर कलेक्शन जैसे कि ArrayList, LinkedList आदि द्वारा कार्यान्वित की जाती है। जब हम उन कलेक्शंस में से एक समानान्तर स्ट्रीम का निर्माण करते हैं, तो इसी स्प्लिट इटरेटर का इस्तेमाल कलेक्शन को विभाजित और इट्रेट करने के लिए किया जाएगा। यह बताता है कि समानांतर इनपुट मूल इनपुट स्रोत (फाइल लाइन) स्तर पर क्यों हुआ बल्कि नक्शे के परिणाम (अर्थात रिकॉर्ड पूजो) पर हुआ। क्या मेरी समझ सही है?

  • मेरे मामले में, इनपुट एक फाइल IO स्ट्रीम है। किस स्प्लिट इटेरेटर का उपयोग किया जाएगा?

  • इससे कोई फर्क नहीं पड़ता कि हम parallel()पाइपलाइन में कहां हैं । मूल इनपुट स्रोत हमेशा विभाजित होगा और शेष मध्यवर्ती संचालन लागू किया जाएगा।

    इस स्थिति में, जावा को उपयोगकर्ताओं को मूल स्रोत को छोड़कर पाइपलाइन में कहीं भी समानांतर संचालन की अनुमति नहीं देनी चाहिए। क्योंकि, यह उन लोगों के लिए गलत समझ दे रहा है जो नहीं जानते हैं कि जावा स्ट्रीम आंतरिक रूप से कैसे काम करती है। मुझे पता है कि parallel()ऑपरेशन को स्ट्रीम ऑब्जेक्ट प्रकार के लिए परिभाषित किया गया होगा और इसलिए, यह इस तरह से काम कर रहा है। लेकिन, कुछ वैकल्पिक समाधान प्रदान करना बेहतर है।

  • उपरोक्त कोड स्निपेट में, मैं इनपुट फ़ाइल में हर रिकॉर्ड के लिए एक पंक्ति संख्या जोड़ने की कोशिश कर रहा हूं और इसलिए इसे आदेश दिया जाना चाहिए। हालांकि, मैं doSomeOperation()समानांतर में लागू करना चाहता हूं क्योंकि यह भारी वजन तर्क है। हासिल करने का एक तरीका यह है कि मैं अपना खुद का कस्टमाइज्ड स्प्लिट इटेटर लिखूं। क्या कोई और तरीका है?


2
जावा रचनाकारों ने इंटरफ़ेस डिजाइन करने का निर्णय कैसे लिया, इसके साथ यह अधिक है। आप पाइपलाइन के लिए अपने अनुरोध और सब कुछ एक अंतिम आपरेशन पहले एकत्र नहीं किया जाएगा। parallel()एक सामान्य संशोधक अनुरोध से अधिक कुछ नहीं है जो अंतर्निहित स्ट्रीम ऑब्जेक्ट पर लागू होता है। याद रखें कि केवल एक स्रोत-धारा है यदि आप पाइप के अंतिम संचालन को लागू नहीं करते हैं, जब तक कि "निष्पादित" कुछ भी नहीं है। होने के बाद, आप मूल रूप से जावा डिजाइन विकल्पों पर सवाल उठा रहे हैं। जो राय आधारित है और हम वास्तव में उस पर मदद नहीं कर सकते।
ज़बुज़ार्ड

1
मुझे पूरी तरह से आपकी बात और भ्रम की स्थिति है, लेकिन मुझे नहीं लगता कि बहुत बेहतर समाधान हैं। विधि Streamसीधे इंटरफ़ेस में पेश की जाती है और अच्छा कैस्केडिंग के कारण हर ऑपरेशन Streamफिर से वापस देता है । कल्पना कीजिए कि कोई व्यक्ति आपको कुछ देना चाहता है, Streamलेकिन पहले से ही इस तरह के एक जोड़े को लागू mapकर दिया है। आप, एक उपयोगकर्ता के रूप में, अभी भी यह तय करने में सक्षम होना चाहते हैं कि इसे समानांतर में निष्पादित किया जाए या नहीं। इसलिए आपके लिए parallel()अभी भी कॉल करना संभव है , हालांकि स्ट्रीम पहले से मौजूद है।
ज़बुज़ार्ड

1
इसके अतिरिक्त, मैं यह सवाल करना चाहता हूं कि आप क्रमिक रूप से एक धारा के एक हिस्से को निष्पादित क्यों करना चाहते हैं और फिर, बाद में समानांतर में स्विच करेंगे। यदि धारा समानांतर निष्पादन के लिए अर्हता प्राप्त करने के लिए पहले से ही पर्याप्त है, तो यह संभवतः पाइपलाइन में सब कुछ के लिए भी लागू होता है। तो क्यों नहीं उस हिस्से के लिए समानांतर निष्पादन का उपयोग करें? मुझे लगता है कि अगर आप नाटकीय रूप से flatMapया यदि आप थ्रेड-असुरक्षित विधियों को निष्पादित करते हैं या इसी तरह के आकार को बढ़ाते हैं तो जैसे किनारे मामले हैं ।
झाबुजार्ड

1
@Zabuza मैं जावा डिजाइन पसंद पर सवाल नहीं उठा रहा हूं, लेकिन मैं सिर्फ अपनी चिंता बढ़ा रहा हूं। किसी भी मूल जावा स्ट्रीम उपयोगकर्ता को तब तक एक ही भ्रम हो सकता है जब तक कि वे स्ट्रीम के काम को नहीं समझते हैं। मैं पूरी तरह से हालांकि आपकी 2 टिप्पणी से सहमत हूं। मैंने सिर्फ एक संभावित समाधान पर प्रकाश डाला है जो आपके स्वयं के नकारात्मक पहलू हो सकता है जैसा कि आपने उल्लेख किया है। लेकिन, हम देख सकते हैं कि क्या इसे किसी अन्य तरीके से हल किया जा सकता है। आपकी तीसरी टिप्पणी के बारे में, मैंने अपने विवरण के अंतिम बिंदु में अपने उपयोग के मामले का उल्लेख पहले ही कर दिया है
एक्सप्लोरर

1
@ यूजीन जब Pathस्थानीय फाइल सिस्टम पर होता है और आप हाल ही में JDK का उपयोग कर रहे होते हैं, तो स्प्लिटर को 1024 के गुणक से गुणा करने की तुलना में बेहतर समानांतर प्रसंस्करण क्षमता होगी। लेकिन संतुलित विभाजन कुछ findFirstपरिदृश्यों में काउंटर-उत्पादक भी हो सकता है ...
Holle

जवाबों:


8

यह बताता है कि समानांतरता मूल इनपुट स्रोत (फ़ाइल लाइनों) के स्तर पर क्यों हुई, बल्कि नक्शे के परिणाम (अर्थात रिकॉर्ड पूजो) पर हुई।

संपूर्ण धारा या तो समानांतर या अनुक्रमिक है। हम क्रमिक रूप से या समानांतर चलाने के लिए संचालन के एक सबसेट का चयन नहीं करते हैं।

जब टर्मिनल ऑपरेशन शुरू किया जाता है, तो धारा पाइपलाइन को अनुक्रमिक रूप से निष्पादित किया जाता है या समानांतर में उस धारा के उन्मुखीकरण पर निर्भर करता है जिस पर इसे लागू किया जाता है। [...] जब टर्मिनल ऑपरेशन शुरू किया जाता है, तो स्ट्रीम पाइपलाइन उस धारा के मोड के आधार पर क्रमिक रूप से या समानांतर रूप से निष्पादित की जाती है, जिस पर इसे लागू किया जाता है। एक ही स्रोत

जैसा कि आप उल्लेख करते हैं, समानांतर धाराएं विभाजित पुनरावृत्तियों का उपयोग करती हैं। स्पष्ट रूप से, यह ऑपरेशन शुरू होने से पहले डेटा को विभाजित करना है।


मेरे मामले में, इनपुट एक फाइल IO स्ट्रीम है। किस स्प्लिट इटेरेटर का उपयोग किया जाएगा?

स्रोत को देखते हुए, मुझे लगता है कि यह उपयोग करता है java.nio.file.FileChannelLinesSpliterator


इससे कोई फर्क नहीं पड़ता कि हम पाइपलाइन में कहां समानांतर () जगह रखते हैं। मूल इनपुट स्रोत हमेशा विभाजित होगा और शेष मध्यवर्ती संचालन लागू किया जाएगा।

सही। आप कॉल parallel()और sequential()कई बार भी कर सकते हैं । जो अंतिम आह्वान करेगा वही जीतेगा। जब हम कॉल करते हैं parallel(), तो हम उस धारा के लिए सेट करते हैं जो वापस आ गई है; और जैसा कि ऊपर कहा गया है, सभी ऑपरेशन या तो क्रमिक रूप से या समानांतर रूप से चलते हैं।


इस स्थिति में, जावा उपयोगकर्ताओं को मूल स्रोत को छोड़कर पाइपलाइन में कहीं भी समानांतर संचालन की अनुमति नहीं देनी चाहिए ...

यह राय का विषय बन जाता है। मुझे लगता है कि ज़बुज़ा जेडीके डिजाइनरों की पसंद का समर्थन करने का एक अच्छा कारण देता है।


हासिल करने का एक तरीका यह है कि मैं अपना खुद का कस्टमाइज्ड स्प्लिट इटेटर लिखूं। क्या कोई और तरीका है?

यह आपके ऑपरेशन पर निर्भर करता है

  • यदि 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()), तो शायद आपको कस्टम विभाजन इटेटर की आवश्यकता होगी। इससे पहले कि मैं वहां जाऊं, हालांकि, मैं अन्य विकल्पों पर ध्यान दूंगा (जैसे कि आईडी कॉलम के साथ बचत रेखाएं शुरू करने के लिए - यह सिर्फ मेरी राय है)।
    मैं इस तरह से छोटे बैचों में रिकॉर्ड बनाने की कोशिश करूँगा:

    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
स्पष्टीकरण के लिए धन्यवाद। आपके द्वारा हाइलाइट किए गए तीसरे समाधान के बारे में जानना अच्छा है। मैं एक नज़र डालूंगा क्योंकि मैंने टेकवाइल और सप्लायर का इस्तेमाल नहीं किया है।
एक्सप्लोरर

2
एक कस्टम Spliteratorकार्यान्वयन इससे अधिक जटिल नहीं होगा, जबकि अधिक कुशल समानांतर प्रसंस्करण की अनुमति देता है ...
Holger

1
आपके प्रत्येक आंतरिक parallelStreamऑपरेशन में ऑपरेशन शुरू करने और अंतिम परिणाम की प्रतीक्षा करने के लिए एक निश्चित ओवरहेड है, जबकि एक समानांतरवाद तक सीमित है batchSize। सबसे पहले, आपको निष्क्रिय थ्रेड्स से बचने के लिए वर्तमान में उपलब्ध सीपीयू कोर की एक बहु की आवश्यकता है। फिर, निर्धारित ओवरहेड की भरपाई करने के लिए संख्या पर्याप्त होनी चाहिए, लेकिन संख्या जितनी अधिक होगी, समानांतर प्रसंस्करण से पहले हो रहे अनुक्रमिक रीड ऑपरेशन द्वारा लगाए गए ठहराव जितना अधिक होगा।
होल्गर

1
बाहरी धारा को समानांतर में बदलने से वर्तमान कार्यान्वयन में आंतरिक के साथ बुरा हस्तक्षेप होता है, इसके अलावा बिंदु जो Stream.generateएक अनियंत्रित स्ट्रीम का उत्पादन करता है, जो ओपी के इच्छित उपयोग के मामलों के साथ काम नहीं करता है findFirst()। इसके विपरीत, एक छलनी के साथ एक एकल समानांतर धारा जो trySplitसीधे-आगे काम करती है और काम के धागे को पिछले के पूरा होने की प्रतीक्षा किए बिना अगले चंक को संसाधित करने की अनुमति देती है।
होल्गर

2
यह मानने का कोई कारण नहीं है कि एक findFirst()ऑपरेशन केवल बहुत कम तत्वों को संसाधित करेगा। पहला मैच सभी तत्वों के 90% प्रसंस्करण के बाद भी हो सकता है। इसके अलावा, जब दस मिलियन लाइनें होती हैं, तो 10% के बाद भी मैच ढूंढने के लिए एक मिलियन लाइनों की प्रोसेसिंग की आवश्यकता होती है।
होल्गर

7

मूल स्ट्रीम डिज़ाइन में विभिन्न समानांतर निष्पादन सेटिंग्स के साथ बाद के पाइपलाइन चरणों का समर्थन करने का विचार शामिल था, लेकिन इस विचार को छोड़ दिया गया है। एपीआई इस समय से स्टेम कर सकता है, लेकिन दूसरी तरफ, एक एपीआई डिजाइन जो कॉल करने वाले को समानांतर या अनुक्रमिक निष्पादन के लिए एक एकल स्पष्ट निर्णय लेने के लिए मजबूर करता है, और अधिक जटिल होगा।

Spliteratorउपयोग में वास्तविक Files.lines(…)कार्यान्वयन-निर्भर है। Java 8 (Oracle या OpenJDK) में, आप हमेशा की तरह ही मिलते हैं BufferedReader.lines()। और हाल ही में JDKs में, यदि Pathडिफ़ॉल्ट फाइल सिस्टम के अंतर्गत आता है और चारसेट इस सुविधा के लिए सहायता-प्राप्त है, तो आप एक स्ट्रीम एक समर्पित साथ मिल Spliteratorकार्यान्वयन, java.nio.file.FileChannelLinesSpliterator। यदि पूर्व शर्त पूरी नहीं की जाती है, तो आप उसी के साथ मिलते हैं BufferedReader.lines(), जो अभी भी एक Iteratorकार्यान्वित पर आधारित है BufferedReaderऔर इसके माध्यम से लिपटा हुआ है Spliterators.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();
हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.