थ्रेड पूल की तुलना में कांटा / ज्वाइन फ्रेमवर्क कैसे बेहतर है?


134

नए कांटे का उपयोग करने के क्या फायदे हैं / शुरुआत में एन सबस्कूल में बड़े काम को विभाजित करने के लिए फ्रेमवर्क से जुड़ना , उन्हें कैश्ड थ्रेड पूल ( एक्जिक्यूटर्स से ) भेजना और प्रत्येक कार्य के पूरा होने का इंतजार करना? मैं यह देखने में विफल रहता हूं कि कांटा का उपयोग करना / अमूर्तता में शामिल होना समस्या को सरल करता है या समाधान को और अधिक कुशल बनाता है जो हमने वर्षों से किया है।

उदाहरण के लिए, ट्यूटोरियल उदाहरण में समानांतर ब्लरिंग एल्गोरिथ्म को इस तरह लागू किया जा सकता है:

public class Blur implements Runnable {
    private int[] mSource;
    private int mStart;
    private int mLength;
    private int[] mDestination;

    private int mBlurWidth = 15; // Processing window size, should be odd.

    public ForkBlur(int[] src, int start, int length, int[] dst) {
        mSource = src;
        mStart = start;
        mLength = length;
        mDestination = dst;
    }

    public void run() {
        computeDirectly();
    }

    protected void computeDirectly() {
        // As in the example, omitted for brevity
    }
}

शुरुआत में विभाजित करें और थ्रेड पूल को कार्य भेजें:

// source image pixels are in src
// destination image pixels are in dst
// threadPool is a (cached) thread pool

int maxSize = 100000; // analogous to F-J's "sThreshold"
List<Future> futures = new ArrayList<Future>();

// Send stuff to thread pool:
for (int i = 0; i < src.length; i+= maxSize) {
    int size = Math.min(maxSize, src.length - i);
    ForkBlur task = new ForkBlur(src, i, size, dst);
    Future f = threadPool.submit(task);
    futures.add(f);
}

// Wait for all sent tasks to complete:
for (Future future : futures) {
    future.get();
}

// Done!

कार्य थ्रेड पूल की कतार में जाते हैं, जहाँ से वे कामगार थ्रेड उपलब्ध होने पर निष्पादित होते हैं। जब तक बंटवारा पर्याप्त रूप से बारीक होता है (अंतिम कार्य के लिए विशेष रूप से प्रतीक्षा करने से बचने के लिए) और थ्रेड पूल में पर्याप्त (कम से कम एन प्रोसेसर) धागे होते हैं, सभी प्रोसेसर पूरी गति से काम कर रहे हैं जब तक कि पूरी गणना नहीं की जाती है।

क्या मैं कुछ भूल रहा हूँ? कांटा / ज्वाइन फ्रेमवर्क का उपयोग करने का अतिरिक्त मूल्य क्या है?

जवाबों:


136

मुझे लगता है कि बुनियादी गलतफहमी है, कि फोर्क / ज्वाइन उदाहरण काम चोरी नहीं दिखाते हैं लेकिन केवल कुछ प्रकार के मानक विभाजित और जीतते हैं।

काम चोरी इस तरह होगी: श्रमिक बी ने अपना काम खत्म कर दिया है। वह एक दयालु व्यक्ति है, इसलिए वह चारों ओर देखता है और वर्कर ए को अभी भी बहुत मेहनत से देखता है। वह टहलता है और पूछता है: "अरे बालक, मैं तुम्हें एक हाथ दे सकता था।" एक उत्तर दिया। "कूल, मेरे पास 1000 इकाइयों का यह कार्य है। अब तक मैं 655 छोड़कर 345 समाप्त कर चुका हूं। क्या आप कृपया संख्या 673 से 1000 पर काम कर सकते हैं, मैं 346 से 672 करूंगा।" बी कहता है "ठीक है, चलो शुरू करते हैं ताकि हम पहले पब में जा सकें।"

आप देखें - असली काम शुरू करने पर भी श्रमिकों को एक-दूसरे के बीच संवाद करना चाहिए। यह उदाहरणों में गायब हिस्सा है।

दूसरी ओर के उदाहरण केवल "उपमहाद्वीपों का उपयोग करें" जैसे कुछ दिखाते हैं:

कार्यकर्ता A: "डांग, मेरे पास 1000 इकाइयाँ हैं। मेरे लिए बहुत कुछ। मैं खुद 500 करूँगा और किसी और के लिए 500 उपमहाद्वीप करूँगा।" यह तब तक चलता है जब तक कि बड़े कार्य को 10 इकाइयों के छोटे पैकेटों में नहीं तोड़ा जाता। ये उपलब्ध श्रमिकों द्वारा निष्पादित किए जाएंगे। लेकिन अगर एक पैकेट एक प्रकार की जहर की गोली है और अन्य पैकेटों की तुलना में काफी लंबा है - बुरी किस्मत, फूट का दौर खत्म हो गया है।

फोर्क / जॉइन और वर्क अपफ्रंट को विभाजित करने के बीच एकमात्र शेष अंतर यह है: जब अपफ्रंट को विभाजित करने के लिए आपके पास काम की कतार शुरू से ही सही है। उदाहरण: 1000 इकाइयाँ, दहलीज 10 है, इसलिए कतार में 100 प्रविष्टियाँ हैं। ये पैकेट थ्रेड सदस्यों को वितरित किए जाते हैं।

कांटा / जुड़ाव अधिक जटिल है और कतार में पैकेट की संख्या को छोटा रखने की कोशिश करता है:

  • चरण 1: एक पैकेट युक्त (1 ... 1000) कतार में रखें
  • चरण 2: एक कार्यकर्ता पैकेट (1 ... 1000) को पॉप करता है और उसे दो पैकेटों से बदलता है: (1 ... 500) और (501 ... 1000)।
  • चरण 3: एक कार्यकर्ता पैकेट (500 ... 1000) और पुश (500 ... 750) और (751 ... 1000) करता है।
  • चरण n: स्टैक में ये पैकेट होते हैं: (1..500), (500 ... 750), (750 ... 875) ... (991..1000)
  • चरण n + 1: पैकेट (991..1000) पॉप अप और निष्पादित है
  • चरण n + 2: पैकेट (981..990) पॉप अप और निष्पादित है
  • चरण n + 3: पैकेट (961..980) पॉप अप किया गया है और (961 ... 970) और (971..980) में विभाजित है। ....

आप देखते हैं: फोर्क / ज्वाइन में कतार छोटी है (उदाहरण में 6) और "स्प्लिट" और "वर्क" फेज इंटरलीव्ड हैं।

जब कई कार्यकर्ता पॉपिंग कर रहे होते हैं और एक साथ धक्का दे रहे होते हैं तो बातचीत निश्चित रूप से स्पष्ट नहीं होती है।


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

AH यदि आपका उत्तर सही है, तो यह नहीं बताता कि कैसे। ओरेकल द्वारा दिए गए उदाहरण से काम की चोरी नहीं होती है। उदाहरण के लिए, आप यहाँ बताए गए कार्यों के अनुसार कैसे काम करेंगे? क्या आप कुछ जावा कोड दिखा सकते हैं जो कांटा बना सकते हैं और चोरी के काम में शामिल हो सकते हैं जिस तरह से आप इसका वर्णन करते हैं? धन्यवाद
मार्क

@Marc: मुझे क्षमा करें, लेकिन मेरे पास कोई उदाहरण उपलब्ध नहीं है।
एएच

6
ओरेकल के उदाहरण के साथ समस्या, IMO, यह नहीं है कि यह काम चोरी का प्रदर्शन नहीं करता है (यह करता है, जैसा कि एएच द्वारा वर्णित है), लेकिन यह कि एक सरल थ्रेडपूल के लिए एल्गोरिथ्म को कोड करना आसान है जो कि (जैसा कि जूनास ने किया)। FJ सबसे अधिक उपयोगी है जब काम को पर्याप्त स्वतंत्र कार्यों में पूर्व-विभाजन नहीं किया जा सकता है, लेकिन पुनरावर्ती को उन कार्यों में विभाजित किया जा सकता है जो आपस में स्वतंत्र हैं। एक उदाहरण के लिए मेरा उत्तर देखें
ashirley

2
काम चोरी करने के कुछ उदाहरण काम आ सकते हैं: h-online.com/developer/features/…
volley

27

यदि आपके पास स्वतंत्र रूप से 100% से दूर काम करने के लिए एन व्यस्त धागे हैं, तो यह फोर्क-जॉइन (एफजे) पूल में एन थ्रेड्स से बेहतर होगा। लेकिन यह कभी इस तरह से काम नहीं करता है।

इस समस्या को n बराबर टुकड़ों में विभाजित करने में सक्षम नहीं हो सकता है। यहां तक ​​कि अगर आप करते हैं, तो थ्रेड शेड्यूलिंग निष्पक्ष होने का कुछ तरीका है। आप धीमे धागे की प्रतीक्षा करेंगे। यदि आपके पास एक से अधिक कार्य हैं, तो वे प्रत्येक n-Way समानता (आमतौर पर अधिक कुशल) से कम के साथ चल सकते हैं, फिर भी अन्य कार्यों के समाप्त होने पर n-way तक जा सकते हैं।

तो क्यों न हम FJ- आकार के टुकड़ों में समस्या को काटें और उस पर थ्रेड पूल का काम करें। विशिष्ट FJ उपयोग समस्या को छोटे टुकड़ों में काट देता है। यादृच्छिक क्रम में इन्हें करने से एक हार्डवेयर स्तर पर बहुत समन्वय की आवश्यकता होती है। ओवरहेड्स एक हत्यारा होगा। FJ में, कार्यों को एक कतार में रखा जाता है, जो थ्रेड अंतिम आउट प्रथम क्रम (LIFO / स्टैक) में पढ़ता है, और कार्य चोरी (मूल काम में, आमतौर पर) First In First Out (FIFO / "कतार") में किया जाता है। इसका परिणाम यह है कि लंबे समय तक प्रसंस्करण बड़े पैमाने पर क्रमिक रूप से किया जा सकता है, भले ही यह छोटे टुकड़ों में टूट गया हो। (यह भी मामला है कि समस्या को एक बड़े धमाके में छोटे समान आकार के टुकड़ों में तोड़ना तुच्छ नहीं हो सकता है। बिना किसी संतुलन के पदानुक्रम के कुछ रूप से निपटने के लिए कहें।)

निष्कर्ष: FJ एक विषम परिस्थितियों में हार्डवेयर थ्रेड्स के अधिक कुशल उपयोग की अनुमति देता है, जो हमेशा आपके पास एक से अधिक थ्रेड होने पर होगा।


लेकिन FJ सबसे धीमे धागे की प्रतीक्षा क्यों नहीं करेगा? उपसर्गों की एक पूर्व निर्धारित संख्या है, और निश्चित रूप से उनमें से कुछ हमेशा पूरा होने वाला अंतिम होगा। maxSizeमेरे उदाहरण में पैरामीटर को समायोजित करने से एफजे उदाहरण में "बाइनरी बंटवारे" के रूप में लगभग समान उपखंड विभाजन का उत्पादन होगा ( compute()विधि के भीतर किया गया , जो या तो कुछ गणना करता है या उप-मुखौटे भेजता है invokeAll())।
जूनस पुलकका

क्योंकि वे बहुत छोटे हैं - मैं अपने जवाब में जोड़ दूँगा।
टॉम हैटिन -

ठीक है, अगर उप-संख्याओं का क्रम परिमाण (एस) से बड़ा है, जो वास्तव में समानांतर में संसाधित किया जा सकता है (जो समझ में आता है, पिछले एक की प्रतीक्षा करने से बचने के लिए), तो मैं समन्वय मुद्दों को देख सकता हूं। यदि विभाजन माना जाता है कि FJ उदाहरण भ्रामक हो सकता है: यह 100000 की दहलीज का उपयोग करता है, जो कि 1000x1000 की छवि के लिए 16 वास्तविक उपशीर्षक, प्रत्येक प्रसंस्करण 62500 तत्वों का उत्पादन करेगा। 10000x10000 की छवि के लिए 1024 उप-मुखौटे होंगे, जो पहले से ही कुछ है।
जूनस पुलकका

19

थ्रेड पूल और फोर्क / जॉइन का अंतिम लक्ष्य एक जैसे हैं: दोनों उपलब्ध सीपीयू शक्ति का उपयोग करना चाहते हैं जो वे अधिकतम थ्रूपुट के लिए कर सकते हैं। अधिकतम थ्रूपुट का मतलब है कि जितना संभव हो उतने कार्यों को लंबी अवधि में पूरा किया जाना चाहिए। ऐसा करने के लिए क्या आवश्यक है? (निम्नलिखित के लिए हम मान लेंगे कि गणना कार्यों की कोई कमी नहीं है: हमेशा 100% सीपीयू उपयोग के लिए पर्याप्त है। इसके अलावा मैं हाइपर-थ्रेडिंग के मामले में कोर या वर्चुअल कोर के लिए "सीपीयू" का समान रूप से उपयोग करता हूं)।

  1. सीपीयू उपलब्ध होने से कम से कम कई थ्रेड चलने की जरूरत है, क्योंकि कम थ्रेड चलने से एक कोर अप्रयुक्त हो जाएगा।
  2. अधिक से अधिक थ्रेड्स चलने चाहिए जितने में सीपीयू उपलब्ध हैं, क्योंकि अधिक थ्रेड्स चलाने से शेड्यूलर के लिए अतिरिक्त भार पैदा होगा जो विभिन्न थ्रेड्स में सीपीयू को असाइन करता है जिसके कारण कुछ CPU समय हमारे कम्प्यूटेशनल कार्य के बजाय शेड्यूलर में जाता है।

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

तो आप एक थ्रेड पूल के साथ परेशानी में कब पड़ेंगे? ऐसा इसलिए है कि एक थ्रेड ब्लॉक हो जाता है , क्योंकि आपका धागा किसी अन्य कार्य के पूरा होने की प्रतीक्षा कर रहा है। निम्न उदाहरण मान लें:

class AbcAlgorithm implements Runnable {
    public void run() {
        Future<StepAResult> aFuture = threadPool.submit(new ATask());
        StepBResult bResult = stepB();
        StepAResult aResult = aFuture.get();
        stepC(aResult, bResult);
    }
}

जो हम यहां देखते हैं वह एक एल्गोरिथ्म है जिसमें तीन चरण ए, बी और सी। ए और बी शामिल हैं एक दूसरे से स्वतंत्र रूप से किया जा सकता है, लेकिन चरण सी को चरण ए और बी के परिणाम की आवश्यकता है। यह एल्गोरिथ्म क्या करता है कार्य ए प्रस्तुत करना है। थ्रेडपूल और प्रदर्शन कार्य बी सीधे। उसके बाद थ्रेड टास्क ए के रूप में अच्छी तरह से करने के लिए इंतजार करेगा और सी के साथ जारी रहेगा। यदि ए और बी एक ही समय में पूरा हो जाते हैं, तो सब कुछ ठीक है। लेकिन क्या होगा अगर A, B से अधिक समय लेता है? ऐसा इसलिए हो सकता है क्योंकि टास्क A की प्रकृति इसे निर्धारित करती है, लेकिन यह ऐसा भी हो सकता है क्योंकि शुरुआत में उपलब्ध टास्क A के लिए कोई सूत्र नहीं है और कार्य A को प्रतीक्षा करने की आवश्यकता है। (यदि केवल एक ही सीपीयू उपलब्ध है और इस तरह आपके थ्रेडपूल में केवल एक ही धागा है, तो यह गतिरोध का कारण भी बनेगा, लेकिन अब यह बिंदु के अलावा है। मुद्दा यह है कि थ्रेड जो बस कार्य बी को निष्पादित करता हैपूरे धागे को अवरुद्ध करता है । चूँकि हमारे पास सीपीयू के समान धागे होते हैं और एक धागा अवरुद्ध होता है अर्थात एक सीपीयू निष्क्रिय होता है

Fork / Join इस समस्या को हल करता है: fork / join फ्रेमवर्क में आप निम्न के अनुसार एक ही एल्गोरिथ्म लिखेंगे:

class AbcAlgorithm implements Runnable {
    public void run() {
        ATask aTask = new ATask());
        aTask.fork();
        StepBResult bResult = stepB();
        StepAResult aResult = aTask.join();
        stepC(aResult, bResult);
    }
}

वही दिखता है, है ना? हालांकि सुराग यह है कि aTask.join ब्लॉक नहीं होगा । इसके बजाय यहाँ वह जगह है जहाँ काम करना चोरी में आता है: धागा अन्य कार्यों के लिए चारों ओर देखेगा जो अतीत में कांटे गए हैं और उन लोगों के साथ जारी रहेंगे। पहले यह जांचता है कि क्या यह अपने आप काम करता है प्रसंस्करण शुरू कर दिया है। इसलिए यदि A को किसी अन्य थ्रेड द्वारा प्रारंभ नहीं किया गया है, तो यह A अगली बार करेगा, अन्यथा यह अन्य थ्रेड्स की कतार की जाँच करेगा और उनके कार्य को चुरा लेगा। एक बार जब दूसरे धागे का यह कार्य पूरा हो जाता है, तो यह जांच करेगा कि क्या ए अब पूरा हो गया है। यदि यह उपरोक्त एल्गोरिथ्म है तो कॉल कर सकते हैं stepC। अन्यथा यह चोरी करने के लिए एक और कार्य के लिए दिखेगा। इस प्रकार कांटा / जुड़ने वाले पूल 100% सीपीयू उपयोग को प्राप्त कर सकते हैं, यहां तक ​​कि अवरुद्ध कार्यों के कारण भी

हालांकि एक जाल है: एस के joinकॉल के लिए कार्य-चोरी केवल संभव है ForkJoinTask। यह बाहरी अवरोधन क्रियाओं के लिए नहीं किया जा सकता है, जैसे किसी अन्य धागे के लिए प्रतीक्षा करना या I / O कार्रवाई की प्रतीक्षा करना। तो उस बारे में क्या, I / O को पूरा करने के लिए इंतजार करना एक आम काम है? इस मामले में अगर हम फोर्क / जॉइन पूल में एक अतिरिक्त धागा जोड़ सकते हैं जो कि ब्लॉकिंग एक्शन के पूरा होते ही फिर से बंद हो जाएगा, ऐसा करने के लिए दूसरी सबसे अच्छी बात होगी। और ForkJoinPoolवास्तव में कर सकते हैं कि अगर हम एस का उपयोग कर रहे हैं ManagedBlocker

फाइबोनैचि

में RecursiveTask के लिए JavaDoc का उपयोग कर कांटा / जुड़ें फिबोनैकी संख्या की गणना के लिए एक उदाहरण है। क्लासिक पुनरावर्ती समाधान के लिए देखें:

public static int fib(int n) {
    if (n <= 1) {
        return n;
    }
    return fib(n - 1) + fib(n - 2);
}

जैसा कि समझाया गया है कि JavaDocs, यह संख्याओं की गणना के लिए एक बहुत ही अच्छा तरीका है, क्योंकि इस एल्गोरिथ्म में O (2 ^ n) जटिलता है जबकि सरल तरीके संभव हैं। हालांकि यह एल्गोरिथ्म बहुत सरल और समझने में आसान है, इसलिए हम इसके साथ चिपके रहते हैं। मान लेते हैं कि हम फोर्क / जॉइन के साथ इसे गति देना चाहते हैं। एक भोली कार्यान्वयन इस तरह दिखेगा:

class Fibonacci extends RecursiveTask<Long> {
    private final long n;

    Fibonacci(long n) {
        this.n = n;
    }

    public Long compute() {
        if (n <= 1) {
            return n;
        }
        Fibonacci f1 = new Fibonacci(n - 1);
        f1.fork();
        Fibonacci f2 = new Fibonacci(n - 2);
        return f2.compute() + f1.join();
   }
}

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

पूर्णता के लिए: यदि आप वास्तव में इस पुनरावर्ती दृष्टिकोण का उपयोग करके फाइबोनैचि संख्याओं की गणना करना चाहते हैं, तो एक अनुकूलित संस्करण है:

class FibonacciBigSubtasks extends RecursiveTask<Long> {
    private final long n;

    FibonacciBigSubtasks(long n) {
        this.n = n;
    }

    public Long compute() {
        return fib(n);
    }

    private long fib(long n) {
        if (n <= 1) {
            return 1;
        }
        if (n > 10 && getSurplusQueuedTaskCount() < 2) {
            final FibonacciBigSubtasks f1 = new FibonacciBigSubtasks(n - 1);
            final FibonacciBigSubtasks f2 = new FibonacciBigSubtasks(n - 2);
            f1.fork();
            return f2.compute() + f1.join();
        } else {
            return fib(n - 1) + fib(n - 2);
        }
    }
}

यह सबटैक्शंस को बहुत छोटा रखता है क्योंकि वे केवल n > 10 && getSurplusQueuedTaskCount() < 2सच होने पर विभाजित होते हैं , जिसका अर्थ है कि करने के लिए 100 से अधिक विधि कॉल हैं ( n > 10) और पहले से ही इंतजार कर रहे (बहुत) आदमी काम नहीं कर रहे हैं getSurplusQueuedTaskCount() < 2

मेरे कंप्यूटर पर (4 कोर (हाइपर-थ्रेडिंग गिनते समय 8), Intel (R) Core (TM) i7-2720QM CPU @ 2.20GHz) fib(50)क्लासिक दृष्टिकोण के साथ 64 सेकंड और फोर्क / जॉइन एप्रोच के साथ सिर्फ 18 सेकंड का समय लेता है काफी ध्यान देने योग्य लाभ है, हालांकि सैद्धांतिक रूप से जितना संभव नहीं है।

सारांश

  • हां, आपके उदाहरण में Fork / Join का क्लासिक थ्रेड पूल पर कोई लाभ नहीं है।
  • अवरुद्ध होने पर शामिल होने पर कांटे / जुड़ने से प्रदर्शन में काफी सुधार हो सकता है
  • कांटा / परिधि में कुछ गतिरोध की समस्याएं शामिल हैं

17

कांटा / जुड़ाव एक थ्रेड पूल से अलग है क्योंकि यह चोरी का काम करता है। से कांटा / शामिल हों

किसी भी ExecutorService के साथ के रूप में, कांटा / शामिल होने के एक धागा पूल में कार्यकर्ता सूत्र को कार्य वितरित करता है। कांटा / जुड़ने की रूपरेखा अलग है क्योंकि यह एक काम-चोरी एल्गोरिथ्म का उपयोग करता है। वर्कर थ्रेड्स जो चीजों को करने के लिए बाहर निकलती हैं, अन्य थ्रेड्स से कार्यों को चुरा सकती हैं जो अभी भी व्यस्त हैं।

मान लें कि आपके दो धागे हैं, और 4 कार्य a, b, c, d हैं जो क्रमशः 1, 1, 5 और 6 सेकंड लेते हैं। प्रारंभ में, ए और बी को थ्रेड 1 और सी और डी टू थ्रेड के लिए सौंपा गया है। एक थ्रेड पूल में, यह 11 सेकंड लगेगा। कांटा / जुड़ने के साथ, धागा 1 खत्म हो जाता है और धागा 2 से काम चोरी कर सकता है, इसलिए टास्क d अंत में धागा 1 द्वारा निष्पादित किया जाएगा। थ्रेड 1 एक, बी और डी, थ्रेड 2 सिर्फ c निष्पादित करता है। कुल समय: 8 सेकंड, 11 नहीं।

संपादित करें: जैसा कि जूनस बताते हैं, कार्य आवश्यक रूप से एक धागे को पूर्व-आवंटित नहीं होते हैं। कांटा / जुड़ने का विचार यह है कि एक कार्य को कई उप-टुकड़ों में विभाजित करने का विकल्प चुन सकते हैं। तो उपरोक्त को पुनर्स्थापित करने के लिए:

हमारे पास दो कार्य (ab) और (cd) हैं जो क्रमशः 2 और 11 सेकंड लेते हैं। थ्रेड 1 एब को निष्पादित करना शुरू कर देता है और इसे दो उप-कार्यों में विभाजित करता है a & b। इसी तरह थ्रेड 2 के साथ, यह दो उप-कार्य c & d में विभाजित होता है। जब थ्रेड 1 ने ए और बी समाप्त कर दिया है, तो यह थ्रेड 2 से डी चुरा सकता है।


5
थ्रेड पूल आमतौर पर ThreadPoolExecutor इंस्टेंस हैं। ऐसे में, कार्य एक कतार में चले जाते हैं ( व्यवहार में ब्लॉकिंग क्यू ), जिससे कार्यकर्ता थ्रेड्स कार्य लेते हैं जैसे ही वे अपने पिछले कार्य को पूरा करते हैं। जहां तक ​​मैं समझता हूं, कार्य विशिष्ट थ्रेड्स को पूर्व-असाइन नहीं किए गए हैं। प्रत्येक थ्रेड में एक समय में (अधिकतम) 1 कार्य होता है।
जूनस पुलकका

4
AFAIK वहाँ है एक के लिए कतार एक ThreadPoolExecutor जो बारी नियंत्रण में कई धागे। इसका मतलब यह है कि एक निष्पादक को कार्य या रनवेबल्स (थ्रेड्स नहीं!) असाइन करना, कार्य एक विशिष्ट थ्रेड्स को भी प्रचारित नहीं किया जाता है। ठीक उसी तरह जिस तरह से एफजे भी करता है। अभी तक FJ का उपयोग करने के लिए कोई लाभ नहीं है।
एएच

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

1
@ मैथ्यू फैवेल: एफजे उदाहरण में , प्रत्येक कार्य के भीतर, compute()या तो कार्य की गणना करता है, या इसे दो उप-प्रकारों में विभाजित करता है। यह कौन सा विकल्प चुनता है यह केवल कार्य के आकार ( if (mLength < sThreshold)...) पर निर्भर करता है , इसलिए यह निश्चित संख्या में कार्य बनाने का एक फैंसी तरीका है। 1000x1000 की छवि के लिए, वास्तव में 16 सबटुक होंगे जो वास्तव में किसी चीज़ की गणना करते हैं। इसके अतिरिक्त 15 (= 16 - 1) "मध्यवर्ती" कार्य होंगे जो केवल उप-उत्पन्न और आह्वान करते हैं और स्वयं कुछ भी गणना नहीं करते हैं।
जूनस पुलकका

2
@ मैथ्यू फेयरवेल: यह संभव है कि मैं सभी एफजे को नहीं समझ पाऊं, लेकिन अगर किसी उपमा ने अपनी computeDirectly()पद्धति को अंजाम देने का फैसला किया है , तो किसी भी चीज को चुराने का कोई तरीका नहीं है। पूरे विभाजन को प्राथमिकता से किया जाता है , कम से कम उदाहरण में।
जूनस पुलका

14

उपरोक्त सभी सही है कि काम चोरी से प्राप्त किया जाता है, लेकिन यह क्यों है इस पर विस्तार करने के लिए।

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

इसका परिणाम यह है:

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

थ्रेड पूल का उपयोग करने वाली अधिकांश अन्य विभाजित और जीत की योजनाओं के लिए अधिक अंतर-थ्रेड संचार और समन्वय की आवश्यकता होती है।


13

इस उदाहरण में Fork / Join का कोई मूल्य नहीं है क्योंकि फोर्किंग की आवश्यकता नहीं है और वर्कलोड समान रूप से वर्कर थ्रेड में विभाजित है। फोर्क / जॉइन केवल ओवरहेड जोड़ता है।

यहाँ इस विषय पर एक अच्छा लेख है । उद्धरण:

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


8

एक और महत्वपूर्ण अंतर यह प्रतीत होता है कि एफजे के साथ, आप कई, जटिल "जॉइन" चरण कर सकते हैं। Http://facademy.ycp.edu/~dhovemey/spring2011/cs365/lecture/lecture18.html से मर्ज सॉर्ट पर विचार करें , इस कार्य को पूर्व-विभाजित करने के लिए बहुत अधिक ऑर्केस्ट्रेशन की आवश्यकता होगी। उदा। आपको निम्नलिखित चीजें करने की आवश्यकता है:

  • पहली तिमाही छाँटें
  • दूसरी तिमाही को छाँटें
  • पहले 2 तिमाहियों को मर्ज करें
  • तीसरी तिमाही को छाँटें
  • अगली तिमाही को छाँटें
  • अंतिम 2 तिमाहियों को मर्ज करें
  • 2 हिस्सों को मिलाएं

आप यह कैसे निर्दिष्ट करते हैं कि आप मर्ज करने से पहले किस तरह का काम करते हैं जो उन्हें चिंतित करता है आदि।

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


6

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

1 2 3 .. 7 3 1 .... 8 5 4
4 5 6 + 2 4 3 => 6 9 9
7 8 9 .. 1 1 0 0 - 8 9 9


3

आप क्रॉलर जैसे एप्लिकेशन में ForkJoin के प्रदर्शन पर चकित होंगे। यहाँ सबसे अच्छा ट्यूटोरियल है जो आप से सीखेंगे।

फोर्क / जॉइन का तर्क बहुत सरल है: (1) प्रत्येक बड़े कार्य को छोटे कार्यों में अलग करना (कांटा); (2) प्रत्येक कार्य को एक अलग थ्रेड में संसाधित करें (यदि आवश्यक हो तो उन्हें छोटे कार्यों में भी अलग करें); (३) परिणामों में शामिल हों।


3

यदि समस्या ऐसी है कि हमें अन्य थ्रेड्स के पूरा होने की प्रतीक्षा करनी है (जैसे कि एरे या सरणी के योग के मामले में), कांटा जुड़ने का उपयोग किया जाना चाहिए, क्योंकि एक्सेक्यूटर (एक्जिक्यूटर्स ।newFixedThreadPool (2)) सीमित होने के कारण चोक हो जाएगा धागों की संख्या। एक ही समानता को बनाए रखने के लिए अवरुद्ध थ्रेड के लिए कवर करने के लिए forkjoin पूल इस मामले में अधिक थ्रेड्स बनाएगा

स्रोत: http://www.oracle.com/technetwork/articles/java/fork-join-422606.html

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

डॉग ली के प्रयासों के माध्यम से जावा एसई 7 में java.util.concurrent पैकेज में जोड़ा गया कांटा / ज्वाइन फ्रेमवर्क इस गैप को पूरा करता है।

स्रोत: https://docs.oracle.com/javase/7/docs/api/java/util/concurrent/ForkJoinPool.html

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

सार्वजनिक int getPoolSize () उन श्रमिक थ्रेडों की संख्या लौटाता है जो शुरू हो गए हैं लेकिन अभी तक समाप्त नहीं हुए हैं। इस विधि द्वारा दिया गया परिणाम getParallelism () से भिन्न हो सकता है, जब थ्रेड समानांतर बनाए रखने के लिए बनाए जाते हैं जब अन्य सहकारी रूप से अवरुद्ध होते हैं।


2

मैं उन लोगों के लिए एक संक्षिप्त उत्तर जोड़ना चाहूंगा जिनके पास लंबे उत्तर पढ़ने के लिए अधिक समय नहीं है। तुलना पुस्तक एप्लाइड अक्का पैटर्न से ली गई है:

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

हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.