अद्यतन करें और अलग थ्रेड्स में प्रस्तुत करें


12

मैं एक साधारण 2 डी गेम इंजन बना रहा हूं और यह जानने के लिए कि यह कैसे किया जाता है, अलग-अलग थ्रेड्स में स्प्राइट्स को अपडेट और रेंडर करना चाहता हूं।

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

Thread 1 -------------------------- Thread 2
Update obj ------------------------ wait for swap
Create queue ---------------------- render the queue
Wait for render ------------------- notify render done
Swap render queues ---------------- notify swap done

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

यह समस्या है:

औसत CPU उपयोग लगभग 0.1% है। कभी-कभी यह 25% (एक क्वाड कोर पीसी में) तक जाता है। इसका मतलब है कि एक थ्रेड दूसरे की प्रतीक्षा कर रहा है क्योंकि प्रतीक्षा फ़ंक्शन एक परीक्षण और सेट फ़ंक्शन के साथ एक लूप है, और थोड़ी देर लूप आपके सभी सीपीयू संसाधनों का उपयोग करेगा।

मेरा पहला सवाल है: क्या दो धागों को समेटने का एक और तरीका है? मैंने देखा कि std::mutex::lockसीपीयू का उपयोग न करें जबकि यह एक संसाधन को लॉक करने के लिए इंतजार कर रहा है, इसलिए यह थोड़ी देर का लूप नहीं है। यह कैसे काम करता है? मैं उपयोग नहीं कर सकता std::mutexक्योंकि मुझे उन्हें एक धागे में बंद करना होगा और दूसरे धागे में अनलॉक करना होगा।

दूसरा प्रश्न है; चूंकि कार्यक्रम हमेशा 60 एफपीएस पर चलता है, इसलिए कभी-कभी इसका सीपीयू उपयोग 25% तक हो जाता है, जिसका अर्थ है कि दोनों में से एक प्रतीक्षा बहुत इंतजार कर रही है? (दो धागे दोनों 60fps तक सीमित हैं, इसलिए उन्हें आदर्श रूप से बहुत अधिक सिंक्रनाइज़ेशन की आवश्यकता नहीं होगी)।

संपादित करें: सभी उत्तरों के लिए धन्यवाद। पहले मैं कहना चाहता हूं कि मैं रेंडर के लिए एक नया धागा शुरू नहीं करता हूं। मैं शुरू में अद्यतन और रेंडर लूप दोनों शुरू करता हूं। मुझे लगता है कि मल्टीथ्रेडिंग कुछ समय बचा सकता है: मेरे पास निम्नलिखित कार्य हैं: FastAlg () और Alg ()। Alg () मेरे अपडेट ओब्जेक्ट और रेंडर ऑबज दोनों हैं और Fastalg () मेरा "रेंडर रेंडर टू" रेंडर "है। एक ही धागे में:

Alg() //update 
FastAgl() 
Alg() //render

दो धागे में:

Alg() //update  while Alg() //render last frame
FastAlg() 

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

मुझे पता है कि नींद एक अच्छा विचार नहीं है, हालांकि मुझे कभी समस्या नहीं होती है। क्या यह बेहतर होगा?

While(true) 
{
   If(timer.gettimefromlastcall() >= 1/fps)
   Do_update()
}

लेकिन यह एक अनंत होगा जब लूप सभी सीपीयू का उपयोग करेगा। क्या मैं उपयोग को सीमित करने के लिए नींद (एक संख्या <15) का उपयोग कर सकता हूं? इस तरह से, यह उदाहरण के लिए, 100 एफपीएस पर चलेगा, और अपडेट फ़ंक्शन को प्रति सेकंड सिर्फ 60 बार कहा जाएगा।

दो थ्रेड्स को सिंक्रनाइज़ करने के लिए मैं createSemaphore के साथ waforsingleobject का उपयोग करूंगा ताकि मैं अलग-अलग थ्रेड में लॉक और अनलॉक कर सकूं (कुछ समय बाद लूप का उपयोग करके व्हाइटआउट), क्या मैं नहीं करूंगा?


5
"यह मत कहो कि मेरी मल्टीथ्रेडिंग इस मामले में बेकार है, मैं बस यह सीखना चाहता हूं कि यह कैसे करना है" - इस मामले में आपको चीजों को ठीक से सीखना चाहिए, (यह है कि फ्रेम को नियंत्रित करने के लिए नींद का उपयोग न करें) , कभी नहीं , और (बी) थ्रेड-प्रति-घटक डिज़ाइन से बचें और लॉकस्टेप चलाने से बचें, इसके बजाय कार्यों में विभाजन कार्य करें और कार्य कतार से कार्यों को संभालें।
डेमोन

1
@Damon (a) स्लीप () को फ्रेम रेट मैकेनिज्म के रूप में इस्तेमाल किया जा सकता है और वास्तव में यह काफी लोकप्रिय है, हालांकि मुझे इस बात से सहमत होना होगा कि कहीं बेहतर विकल्प हैं। (b) यहां उपयोगकर्ता अपडेट को अलग करना और दो अलग-अलग थ्रेड में रेंडर करना चाहता है। यह एक गेम इंजन में एक सामान्य अलगाव है और ऐसा "थ्रेड-प्रति-घटक" नहीं है। यह स्पष्ट लाभ देता है लेकिन अगर गलत तरीके से किया जाता है तो यह समस्या ला सकता है।
अलेक्जेंड्रे डेबिएंस

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

@Damon जब मैंने कहा कि यह लोकप्रिय था, या आम, मेरे कहने का मतलब यह नहीं था कि यह सही था। मेरा मतलब सिर्फ यह था कि यह कई लोगों द्वारा इस्तेमाल किया गया था। "... हालांकि मुझे इस बात से सहमत होना है कि कहीं बेहतर विकल्प हैं" इसका मतलब है कि यह वास्तव में समय को सिंक्रनाइज़ करने का एक बहुत अच्छा तरीका नहीं है। गलतफहमी के लिए खेद है।
अलेक्जेंड्रे डेबिएंस

@AlphSpirit: कोई चिंता नहीं है :) दुनिया सामान से भरी हुई है जो बहुत से लोग करते हैं (और हमेशा अच्छे कारण के लिए नहीं), लेकिन जब कोई सीखना शुरू कर देता है, तब भी किसी को सबसे स्पष्ट रूप से गलत लोगों से बचने की कोशिश करनी चाहिए।
डेमन

जवाबों:


25

स्प्राइट के साथ एक सरल 2 डी इंजन के लिए, एक एकल-थ्रेडेड दृष्टिकोण पूरी तरह से अच्छा है। लेकिन चूंकि आप मल्टीथ्रेडिंग करना सीखना चाहते हैं, इसलिए आपको इसे सही तरीके से करना सीखना चाहिए।

ऐसा न करें

  • 2 थ्रेड्स का उपयोग करें जो अधिक या कम लॉक-स्टेप चलाते हैं, कई थ्रेड्स के साथ एकल-थ्रेडेड व्यवहार को लागू करते हैं। यह समानता (शून्य) के समान स्तर है, लेकिन संदर्भ स्विच और सिंक्रनाइज़ेशन के लिए ओवरहेड जोड़ता है। इसके अलावा, तर्क कठिन है।
  • sleepफ्रेम दर को नियंत्रित करने के लिए उपयोग करें । कभी नहीँ। अगर कोई तुमसे कहता है, उन्हें मारो।
    सबसे पहले, सभी मॉनिटर 60 हर्ट्ज पर नहीं चलते हैं। दूसरा, एक ही दर से चलने वाले दो टाइमर एक-दूसरे के साथ-साथ चल रहे होते हैं और अंत में सिंक से बाहर निकल जाते हैं (एक ही ऊंचाई से दो पिंगपॉन्ग बॉल्स को छोड़ दें, और सुनें)। तीसरा, sleepहै डिजाइन द्वारा न तो सही है और न ही विश्वसनीय। दानेदारता 15.6ms (वास्तव में, विंडोज [1] पर डिफ़ॉल्ट ) के रूप में खराब हो सकती है , और 60fps पर एक फ्रेम केवल 16.6ms है, जो बाकी सब के लिए मात्र 1ms छोड़ देता है। इसके अलावा, 16.6 को 15.6 का गुणक मिलना मुश्किल है ...
    इसके अलावा, sleep(और कभी-कभी!) 30 या 50 या 100 एमएस या केवल एक लंबे समय के बाद ही वापस आने की अनुमति दी जाती है।
  • std::mutexएक और सूत्र को सूचित करने के लिए उपयोग करें । यह वह नहीं है जो इसके लिए है।
  • यह मान लें कि टास्कमैन आपको बता रहा है कि क्या हो रहा है, विशेष रूप से "25% सीपीयू" जैसी संख्या से, जो आपके कोड में या usermode ड्राइवर के भीतर, या कहीं और खर्च किया जा सकता है।
  • उच्च स्तरीय घटक प्रति एक धागा है (निश्चित रूप से कुछ अपवाद हैं)।
  • "यादृच्छिक समय" पर थ्रेड्स बनाएँ, तदर्थ, प्रति कार्य। थ्रेड बनाना आश्चर्यजनक रूप से महंगा हो सकता है और वे आश्चर्यजनक रूप से लंबे समय तक ले सकते हैं इससे पहले कि वे तीव्रता से कर रहे हैं जो आपने उन्हें बताया था (खासकर यदि आपके पास बहुत सारे डीएलएल लोड हैं!)।

करना

  • चीजों को अतुल्यकालिक रूप से चलाने के लिए मल्टीथ्रेडिंग का उपयोग करेंजितना हो सके, । गति थ्रेडिंग का मुख्य विचार नहीं है, लेकिन चीजों को समानांतर में करना (इसलिए भले ही वे पूरी तरह से अधिक समय लेते हैं, सभी का योग अभी भी कम है)।
  • फ्रेम दर को सीमित करने के लिए ऊर्ध्वाधर सिंक का उपयोग करें। इसे करने का एकमात्र सही (और गैर-विफल) तरीका है। यदि उपयोगकर्ता आपको डिस्प्ले ड्राइवर के नियंत्रण कक्ष ("बलपूर्वक बंद") में ओवरराइड करता है, तो ऐसा हो। आखिर यह उसका कंप्यूटर है, आपका नहीं।
  • यदि आपको नियमित अंतराल पर कुछ "टिक" करने की आवश्यकता है, तो टाइमर का उपयोग करेंsleep[2] की तुलना में टाइमर की बेहतर सटीकता और विश्वसनीयता होने का फायदा है । इसके अलावा, एक रिकरिंग टाइमर समय के लिए सही ढंग से (समय के बीच में गुजरता है) के लिए खाता है जबकि सोते समय 16.6ms (या 16.6 मिनट के लिए मापा गया min_time_elapsed) नहीं करता है।
  • भौतिकी सिमुलेशन चलाएं जिसमें एक निश्चित समय कदम पर संख्यात्मक एकीकरण शामिल है (या आपके समीकरण फट जाएंगे!), ग्राफिक्स के बीच अंतर करें ( हो सकता है) एक अलग-प्रति-घटक-थ्रेड के लिए एक बहाना सकता है, लेकिन यह बिना भी किया जा सकता है)।
  • का प्रयोग करें std::mutexएक समय ( "परस्पर को बाहर") में केवल एक ही धागे का उपयोग एक संसाधन है, और अजीब अर्थ विज्ञान का अनुपालन करने के std::condition_variable
  • संसाधनों के लिए प्रतिस्पर्धा करने से बचें। आवश्यक के रूप में कम के रूप में ताला (लेकिन कम नहीं!) और केवल आवश्यक रूप से लंबे समय तक ताले पकड़ो।
  • थ्रेड्स के बीच रीड-ओनली डेटा (नो कैश इश्यूज, और नो लॉकिंग आवश्यक) साझा करें, लेकिन समवर्ती रूप से डेटा को संशोधित न करें (सिंक की जरूरत है और कैश को मारता है)। जिसमें पास में डेटा को संशोधित करना शामिल है का स्थान है जिसे कोई और व्यक्ति पढ़ सकता है।
  • std::condition_variableजब तक कुछ स्थिति सत्य न हो, किसी अन्य थ्रेड को ब्लॉक करने के लिए उपयोग करें । std::condition_variableउस अतिरिक्त म्यूटेक्स के शब्दार्थ को बड़े ही अजीब ढंग से और मुड़ (ज्यादातर ऐतिहासिक कारणों से पोसिक्स थ्रेड्स से विरासत में मिला) के लिए दिया जाता है, लेकिन एक शर्त वैरिएबल सही आदिम है जिसे आप चाहते हैं।
    मामले में आप पाते हैंstd::condition_variable साथ सहज होने के बहुत अजीब हैं, तो आप इसके बजाय बस एक विंडोज इवेंट (थोड़ा धीमा) का उपयोग कर सकते हैं या, यदि आप साहसी हैं, तो NtKeyedEvents के आसपास अपना सरल ईवेंट बनाएं (इसमें डरावने निम्न स्तर का सामान शामिल है)। जैसा कि आप DirectX का उपयोग करते हैं, वैसे भी आप पहले से ही विंडोज से बंधे हुए हैं, इसलिए पोर्टेबिलिटी का नुकसान एक बड़ी बात नहीं है।
  • निश्चित आकार के कार्य थ्रेड पूल (जो प्रति कोर एक से अधिक नहीं, हाइपरथ्रेड कोर की गणना नहीं) द्वारा चलाए जा रहे कार्य-आकार के कार्यों में विराम देते हैं। कार्यों को पूरा करने के लिए आश्रित कार्यों (स्वतंत्र, स्वचालित सिंक्रनाइज़ेशन) को पूरा करें। ऐसे कार्य करें, जिनमें कम से कम कुछ सौ गैर-तुच्छ ऑपरेशन हों (प्रत्येक एक डिस्क को पढ़ने के लिए एक प्रकार का अवरोधक संचालन)। कैश-सन्निहित पहुँच को प्राथमिकता दें।
  • प्रोग्राम प्रारंभ में सभी थ्रेड्स बनाएँ।
  • अतुल्यकालिक कार्यों का लाभ उठाएं जो ओएस या ग्राफिक्स एपीआई बेहतर / अतिरिक्त समानता के लिए प्रदान करता है, न केवल कार्यक्रम स्तर पर बल्कि हार्डवेयर (पीसीआई ट्रांसफर, सीपीयू-जीपीयू समानतावाद, डिस्क डीएमए, आदि पर भी विचार करें)।
  • 10,000 अन्य चीजें जिनका मैं उल्लेख करना भूल गया हूं।


[१] जी हाँ, आप शेड्यूलर की दर को १ सेंटीमीटर नीचे सेट कर सकते हैं, लेकिन यह बहुत अधिक संदर्भ स्विच का कारण बनता है, और यह बहुत अधिक बिजली की खपत करता है (ऐसी दुनिया में जहां अधिक से अधिक डिवाइस मोबाइल डिवाइस हैं)। यह भी एक समाधान नहीं है क्योंकि यह अभी भी नींद को और अधिक विश्वसनीय नहीं बनाता है।
[२] एक टाइमर थ्रेड की प्राथमिकता को बढ़ावा देगा, जो इसे एक और समान-प्राथमिकता वाले थ्रेड को मध्य-क्वांटम में बाधित करने और पहले शेड्यूल करने की अनुमति देगा, जो एक अर्ध-आरटी व्यवहार है। यह निश्चित रूप से सही आरटी नहीं है, लेकिन यह बहुत करीब आता है। नींद से जागने का अर्थ केवल यह है कि धागा किसी भी समय निर्धारित होने के लिए तैयार हो जाए, जब भी ऐसा हो।


क्या आप कृपया समझा सकते हैं कि आपको "उच्च स्तर के घटक प्रति एक धागा" क्यों नहीं होना चाहिए? क्या आपका मतलब है कि दो अलग-अलग थ्रेड्स में भौतिकी और ऑडियो मिश्रण नहीं होना चाहिए? मुझे ऐसा न करने का कोई कारण नहीं दिखता।
एल्विस स्ट्रैजडिन्स

3

मुझे यकीन नहीं है कि आप अपडेट और Fender दोनों को 60 तक सीमित करके क्या हासिल करना चाहते हैं। यदि आप उन्हें एक ही मूल्य पर सीमित करते हैं, तो आप उन्हें एक ही सूत्र में रख सकते हैं।

अद्यतन और रेंडर को अलग-अलग थ्रेड में अलग करते समय लक्ष्य को एक दूसरे से "लगभग" स्वतंत्र होना चाहिए, ताकि GPU 500 एफपीएस को प्रस्तुत कर सके और अपडेट लॉजिक अभी भी 60 एफपीएस पर जाता है। आप ऐसा करके बहुत उच्च प्रदर्शन हासिल नहीं करते हैं।

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

std::mutex mutex;
mutex.lock();
// Do sensible stuff here...
mutex.unlock();

स्रोत: http://en.cppreference.com/w/cpp/thread/mutex

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

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


और ब्लॉक कैसे काम करता है?
Liuka

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


1
उपयोग std::lock_guard या समान, नहीं .lock()/ .unlock()। RAII सिर्फ मेमोरी मैनेजमेंट के लिए नहीं है!
ई.पू.
हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.