क्या C ++ 11 में async (लॉन्च :: async) महंगी धागा निर्माण से बचने के लिए थ्रेड पूल को अप्रचलित बनाता है?


117

यह इस सवाल से संबंधित है: क्या std :: C ++ 11 में पिरोया हुआ धागा है? । हालांकि यह सवाल अलग है, इरादा एक ही है:

प्रश्न 1: क्या यह अभी भी महंगा धागा निर्माण से बचने के लिए अपने (या 3-पार्टी लाइब्रेरी) थ्रेड पूल का उपयोग करने के लिए समझ में आता है?

अन्य प्रश्न में निष्कर्ष यह था कि आप std::threadपूल किए जाने पर भरोसा नहीं कर सकते (यह हो सकता है या नहीं भी हो सकता है)। हालांकि, std::async(launch::async)लगता है कि पूल किए जाने की अधिक संभावना है।

यह नहीं लगता कि यह मानक द्वारा मजबूर किया गया है, लेकिन IMHO मुझे उम्मीद है कि सभी अच्छे C ++ 11 कार्यान्वयन थ्रेड पूलिंग का उपयोग करेंगे यदि थ्रेड निर्माण धीमा है। केवल उन प्लेटफार्मों पर जहां एक नया धागा बनाना सस्ता है, मैं उम्मीद करूंगा कि वे हमेशा एक नया धागा रखें।

प्रश्न 2: यह वही है जो मुझे लगता है, लेकिन मेरे पास इसे साबित करने के लिए कोई तथ्य नहीं है। मुझसे बहुत गलत हो सकता है। क्या यह एक शिक्षित अनुमान है?

अंत में, यहाँ मैंने कुछ सैंपल कोड दिए हैं जो पहले दिखाता है कि मुझे लगता है कि थ्रेड क्रिएशन को कैसे व्यक्त किया जा सकता है async(launch::async):

उदाहरण 1:

 thread t([]{ f(); });
 // ...
 t.join();

हो जाता है

 auto future = async(launch::async, []{ f(); });
 // ...
 future.wait();

उदाहरण 2: आग और धागा भूल जाओ

 thread([]{ f(); }).detach();

हो जाता है

 // a bit clumsy...
 auto dummy = async(launch::async, []{ f(); });

 // ... but I hope soon it can be simplified to
 async(launch::async, []{ f(); });

प्रश्न 3: क्या आप asyncसंस्करणों को संस्करण पसंद करेंगे thread?


बाकी अब सवाल का हिस्सा नहीं है, लेकिन केवल स्पष्टीकरण के लिए:

रिटर्न मान को डमी वेरिएबल को क्यों सौंपा जाना चाहिए?

दुर्भाग्य से, वर्तमान C ++ 11 मानक बल जो आप के रिटर्न मान पर कब्जा करते हैं std::async, अन्यथा अन्यथा विध्वंसक को निष्पादित किया जाता है, जो कार्रवाई समाप्त होने तक ब्लॉक करता है। इसे मानक में कुछ त्रुटि माना जाता है (जैसे, हर्ब सटर द्वारा)।

से यह उदाहरण cppreference.com यह अच्छी तरह दिखाता है:

{
  std::async(std::launch::async, []{ f(); });
  std::async(std::launch::async, []{ g(); });  // does not run until f() completes
}

एक और स्पष्टीकरण:

मुझे पता है कि थ्रेड पूल के अन्य वैध उपयोग हो सकते हैं लेकिन इस प्रश्न में मैं केवल महंगी धागा निर्माण लागत से बचने के पहलू में दिलचस्पी रखता हूं

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

थ्रेड-लोकल वैरिएबल आपके थ्रेड पूल के लिए भी एक तर्क हो सकता है, लेकिन मुझे यकीन नहीं है कि यह अभ्यास में प्रासंगिक है:

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

8
"हालांकि, std::async(launch::async)लगता है कि पूल किए जाने की बहुत अधिक संभावना है।" नहीं, मेरा मानना ​​है कि इसका std::async(launch::async | launch::deferred)तालमेल हो सकता है। केवल launch::asyncइस कार्य को नए धागे पर लॉन्च किया जाना चाहिए, भले ही अन्य कार्य चल रहे हों। नीति के साथ launch::async | launch::deferredफिर कार्यान्वयन किस नीति को चुनने के लिए हो जाता है, लेकिन अधिक महत्वपूर्ण बात यह है कि किस नीति को चुनने में देरी होती है। यही है, यह तब तक इंतजार कर सकता है जब तक एक थ्रेड पूल में एक धागा उपलब्ध नहीं हो जाता है और फिर एस्किंस पॉलिसी का चयन करें।
bames53

2
जहाँ तक मुझे पता है केवल VC ++ के साथ थ्रेड पूल का उपयोग किया जाता है std::async()। मैं अभी भी यह देखने के लिए उत्सुक हूं कि वे थ्रेड पूल में गैर-तुच्छ थ्रेड_लोक डिस्ट्रक्टर्स का समर्थन कैसे करते हैं।
bames53

2
@ bames53 मैंने libstdc ++ के माध्यम से कदम रखा, जो कि gcc 4.7.2 के साथ आता है और पाया कि यदि लॉन्च पॉलिसी वास्तव में नहीं है,launch::async तो यह ऐसा व्यवहार करता है जैसे कि यह केवल launch::deferredऔर कभी इसे अतुल्यकालिक रूप से निष्पादित नहीं करता है - इसलिए वास्तव में, libstdc ++ का वह संस्करण "चुनता है" जब तक अन्यथा अन्यथा मजबूर न हो, तब तक हमेशा स्थगित का उपयोग करें।
doug65536

3
@ doug65536 थ्रेड_लोक डिस्ट्रक्टर्स के बारे में मेरा कहना था कि थ्रेड पूल का उपयोग करते समय थ्रेड निकास पर विनाश काफी सही नहीं है। जब कोई कार्य एसिंक्रोनस रूप से चलाया जाता है, तो यह 'जैसे कि एक नए धागे पर' चलाया जाता है, युक्ति के अनुसार, जिसका अर्थ है कि प्रत्येक async कार्य को अपने स्वयं के थ्रेड_लोक ऑब्जेक्ट मिलते हैं। एक थ्रेड पूल आधारित कार्यान्वयन को यह सुनिश्चित करने के लिए विशेष ध्यान रखना पड़ता है कि उसी बैकिंग थ्रेड को साझा करने वाले कार्य अभी भी व्यवहार करते हैं जैसे कि उनकी अपनी थ्रेड_लोक ऑब्जेक्ट हैं। इस कार्यक्रम पर विचार करें: pastebin.com/9nWUT40h
bames53

2
@ bames53 का उपयोग "के रूप में अगर एक नए धागे में" कल्पना में मेरी राय में एक बड़ी गलती थी। std::asyncप्रदर्शन के लिए एक सुंदर चीज हो सकती है - यह मानक शॉर्ट-रनिंग-टास्क निष्पादन प्रणाली हो सकती है, स्वाभाविक रूप से थ्रेड पूल द्वारा समर्थित है। अभी, यह सिर्फ std::threadकुछ बकवास के साथ है, जिससे थ्रेड फ़ंक्शन एक मान वापस करने में सक्षम हो। ओह, और उन्होंने निरर्थक "आस्थगित" कार्यक्षमता जोड़ी जो std::functionपूरी तरह से नौकरी को ओवरलैप करती है ।
doug65536

जवाबों:


54

प्रश्न 1 :

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

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

IMHO, लिनक्स कर्नेल लोगों को थ्रेड क्रिएशन को सस्ता बनाने पर काम करना चाहिए जो वर्तमान में है। लेकिन, मानक C ++ लाइब्रेरी को लागू करने के लिए पूल का उपयोग करने पर भी विचार करना चाहिए launch::async | launch::deferred

और ओपी सही है, ::std::threadनिश्चित रूप से एक थ्रेड लॉन्च करने के लिए एक पूल से एक का उपयोग करने के बजाय एक नए थ्रेड के निर्माण को मजबूर करता है। इसलिए ::std::async(::std::launch::async, ...)पसंद किया जाता है।

प्रश्न 2 :

हां, मूल रूप से यह 'अंतर्निहित' एक धागा लॉन्च करता है। लेकिन वास्तव में, यह अभी भी काफी स्पष्ट है कि क्या हो रहा है। इसलिए मुझे नहीं लगता कि वास्तव में यह शब्द विशेष रूप से अच्छा शब्द है।

मैं यह भी नहीं मानता हूं कि विनाश से पहले वापसी के लिए प्रतीक्षा करने के लिए आपको मजबूर करना एक त्रुटि है। मुझे नहीं पता कि आपको async'डेमॉन' थ्रेड्स बनाने के लिए कॉल का उपयोग करना चाहिए जो कि वापस आने की उम्मीद नहीं है। और अगर उनसे वापसी की उम्मीद की जाती है, तो अपवादों की अनदेखी करना ठीक नहीं है।

प्रश्न 3 :

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

मुझे 'भविष्य' के मॉडल की तुलना में काम कतार मॉडल बहुत अच्छा लगा, क्योंकि चारों ओर 'सीरियल के द्वीप' पड़े हुए हैं ताकि आप अधिक प्रभावी ढंग से उत्परिवर्ती स्थिति को संभाल सकें।

लेकिन वास्तव में, यह वास्तव में आप क्या कर रहे हैं पर निर्भर करता है।

प्रदर्शन का परीक्षण

इसलिए, मैंने चीजों को कॉल करने के विभिन्न तरीकों के प्रदर्शन का परीक्षण किया और इन नंबरों के साथ 8 कोर (AMD Ryzen 7 2700X) सिस्टम पर चल रहा है, जो Fedora 29 को क्लेंग वर्जन 7.0.1 और libc ++ (नहीं libstdc ++) के साथ संकलित करता है।

   Do nothing calls per second:   35365257                                      
        Empty calls per second:   35210682                                      
   New thread calls per second:      62356                                      
 Async launch calls per second:      68869                                      
Worker thread calls per second:     970415                                      

और देशी, मेरे मैकबुक प्रो 15 "(Intel (R) Core (TM) i7-7820HQ CPU @ 2.90GHz) पर Apple LLVM version 10.0.0 (clang-1000.10.44.4)OSX 10.13.6 के तहत, मुझे यह मिला:

   Do nothing calls per second:   22078079
        Empty calls per second:   21847547
   New thread calls per second:      43326
 Async launch calls per second:      58684
Worker thread calls per second:    2053775

वर्कर थ्रेड के लिए, मैंने एक थ्रेड शुरू किया, फिर दूसरे थ्रेड में रिक्वेस्ट भेजने के लिए लॉकलेस कतार का इस्तेमाल किया और फिर "इट्स हो गया" रिप्लाई का इंतजार किया।

"कुछ भी नहीं" सिर्फ टेस्ट हार्नेस के ओवरहेड का परीक्षण करना है।

यह स्पष्ट है कि एक थ्रेड लॉन्च करने का ओवरहेड विशाल है। और यहां तक ​​कि इंटर-थ्रेड कतार के साथ श्रमिक धागा एक वीएम में फेडोरा 25 पर 20 या इतने पर कारक द्वारा धीमा हो जाता है, और देशी ओएस एक्स पर लगभग 8।

मैंने प्रदर्शन परीक्षण के लिए उपयोग किए गए कोड को धारण करते हुए एक बिटबकेट परियोजना बनाई। यह यहां पाया जा सकता है: https://bitbucket.org/omnifarious/launch_thread_performance


3
मैं कार्य-कतार मॉडल पर सहमत हूं, हालांकि इसके लिए "पाइपलाइन" मॉडल की आवश्यकता होती है जो समवर्ती पहुंच के हर उपयोग के लिए लागू नहीं हो सकता है।
मथिउ एम।

1
मुझे लगता है कि अभिव्यक्ति टेम्पलेट्स (ऑपरेटरों के लिए) का उपयोग परिणामों की रचना करने के लिए किया जा सकता है, फ़ंक्शन कॉल के लिए आपको कॉल विधि की आवश्यकता होगी जो मुझे लगता है लेकिन ओवरलोड के कारण यह थोड़ा अधिक कठिन हो सकता है।
मैथ्यू एम।

3
"बहुत सस्ता" आपके अनुभव के सापेक्ष है। मुझे लगता है कि लिनक्स थ्रेड क्रिएशन ओवरहेड मेरे उपयोग के लिए पर्याप्त है
जेफ

1
@ जेफ़ - मुझे लगा कि यह उससे बहुत सस्ता है। मैंने अपने उत्तर की थोड़ी देर पहले अद्यतन किया कि मैं वास्तविक लागत की खोज के लिए किए गए परीक्षण को प्रतिबिंबित करूं।
सर्वव्यापी

4
पहले भाग में, आप कुछ हद तक कम आंक रहे हैं कि एक खतरा पैदा करने के लिए कितना कुछ किया जाना चाहिए, और एक फ़ंक्शन को कॉल करने के लिए कितना कम किया जाना चाहिए। एक फ़ंक्शन कॉल और रिटर्न कुछ सीपीयू निर्देश हैं जो स्टैक के शीर्ष पर कुछ बाइट्स में हेरफेर करते हैं। एक धमकी निर्माण का अर्थ है: 1. एक स्टैक आवंटित करना, 2. एक सीस्केल प्रदर्शन करना, 3. कर्नेल में डेटा संरचनाएं बनाना और उन्हें ऊपर से जोड़ना, ताले को रास्ते से पकड़ना, 4. थ्रेड को निष्पादित करने के लिए अनुसूचक की प्रतीक्षा करना, 5. स्विच करना धागे के संदर्भ में। इनमें से प्रत्येक चरण अपने आप में सबसे जटिल फ़ंक्शन कॉल की तुलना में अधिक समय लेता है
cmaster - मोनिका
हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.