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