धागे के पार एक साझा चर को उत्प्रेरण कोड स्पष्ट रूप से एक दौड़ की स्थिति से ग्रस्त क्यों नहीं है?


107

मैं Cygwin GCC का उपयोग कर रहा हूं और इस कोड को चलाता हूं:

#include <iostream>
#include <thread>
#include <vector>
using namespace std;

unsigned u = 0;

void foo()
{
    u++;
}

int main()
{
    vector<thread> threads;
    for(int i = 0; i < 1000; i++) {
        threads.push_back (thread (foo));
    }
    for (auto& t : threads) t.join();

    cout << u << endl;
    return 0;
}

रेखा के साथ संकलित g++ -Wall -fexceptions -g -std=c++14 -c main.cpp -o main.o:।

यह 1000 प्रिंट करता है, जो सही है। हालाँकि, मैंने थ्रेड के कारण कम संख्या की अपेक्षा की थी, जो पहले से बढ़े हुए मान को अधिलेखित कर रहा था। यह कोड पारस्परिक पहुंच से ग्रस्त क्यों नहीं है?

मेरी परीक्षण मशीन में 4 कोर हैं, और मुझे उस कार्यक्रम पर कोई प्रतिबंध नहीं है जिसे मैं जानता हूं।

समस्या तब बनी रहती है जब साझा की गई सामग्री को fooकुछ अधिक जटिल के साथ प्रतिस्थापित किया जाता है , जैसे

if (u % 3 == 0) {
    u += 4;
} else {
    u -= 1;
}

66
इंटेल सीपीयू में एसएमपी सिस्टम (जैसे डुअल पेंटियम प्रो मशीनों) में इस्तेमाल किए गए बहुत शुरुआती x86 सीपीयू के साथ संगतता बनाए रखने के लिए कुछ अद्भुत आंतरिक "शूट डाउन" तर्क है। विफलता की बहुत सारी स्थितियाँ जो हमें सिखाई जाती हैं, लगभग संभव नहीं है वास्तव में x86 मशीनों पर। तो कहते हैं कि एक कोर uस्मृति में वापस लिखने के लिए जाता है । सीपीयू वास्तव में नोटिस जैसी आश्चर्यजनक चीजें करेगा uजो सीपीयू के कैश में मेमोरी लाइन नहीं है और यह वेतन वृद्धि ऑपरेशन को फिर से शुरू करेगा। यही कारण है कि x86 से अन्य आर्किटेक्चर में जाना एक आंख खोलने का अनुभव हो सकता है!
डेविड श्वार्ट्ज

1
शायद अभी भी जल्दी है। आपको यह सुनिश्चित करने के लिए कोड जोड़ने की आवश्यकता है कि थ्रेड की पैदावार होने से पहले यह सुनिश्चित करने के लिए कि अन्य थ्रेड लॉन्च होने से पहले कुछ भी नहीं करता है।
रोब के

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

5
दरअसल, प्रोग्राम को एक लूप में बार-बार दबाने से अंततः पता चलता है कि यह टूट गया है: while true; do res=$(./a.out); if [[ $res != 1000 ]]; then echo $res; break; fi; done;मेरे सिस्टम पर 999 या 998 प्रिंट जैसा कुछ ।
डैनियल कामिल कोजार

जवाबों:


266

foo()इतना छोटा है कि प्रत्येक धागा शायद अगले एक से पहले ही खत्म हो जाता है। यदि आप foo()पहले एक यादृच्छिक समय के लिए नींद जोड़ते हैं u++, तो आप यह देखना शुरू कर सकते हैं कि आप क्या उम्मीद करते हैं।


51
यह वास्तव में अपेक्षित तरीके से आउटपुट को बदल दिया।
mafu

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

हमने हाल ही में C # के साथ यह मुद्दा रखा था। कोड लगभग कभी भी विफल नहीं हुआ, लेकिन बीच में एक एपीआई कॉल के हालिया जोड़ ने इसे लगातार परिवर्तन करने के लिए पर्याप्त देरी की शुरुआत की।
ओब्सीडियन फीनिक्स

@MatthieuM। क्या Microsoft के पास एक स्वचालित उपकरण नहीं है जो बिल्कुल वैसा ही करता है, जैसे कि दौड़ की स्थिति का पता लगाने और उन्हें मज़बूती से प्रतिलिपि प्रस्तुत करने के लिए दोनों की एक विधि के रूप में?
मेसन व्हीलर

1
@MasonWheeler: मैं विशेष रूप से लिनक्स पर nigh काम करता हूं, इसलिए ... dunno :(
Matthieu M.

59

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

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

यदि आपकी मशीन में वृद्धि एक परमाणु ऑप है, तो यह सही ढंग से चलेगा भले ही भाषा मानक के अनुसार यह पूर्वनिर्धारित व्यवहार हो।

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

विशेष रूप से मैं विधानसभा का उपयोग कर के लिए अपने कोड संकलित https://godbolt.org/ और foo()करने के लिए compiles:

foo():
        add     DWORD PTR u[rip], 1
        ret

इसका मतलब है कि यह पूरी तरह से एक ऐड इंस्ट्रक्शन का प्रदर्शन कर रहा है, जो एक प्रोसेसर के लिए परमाणु होगा (हालांकि जैसा कि ऊपर उल्लेख किया गया है कि एक मल्टी प्रोसेसर सिस्टम के लिए नहीं)।


41
यह याद रखना महत्वपूर्ण है कि "जैसा कि चल रहा है" अपरिभाषित व्यवहार का एक स्वीकार्य परिणाम है।
मार्क

3
जैसा आपने संकेत दिया था, यह निर्देश एक एसएमपी मशीन पर परमाणु नहीं है (जो सभी आधुनिक सिस्टम हैं)। यहां तक ​​कि inc [u]परमाणु भी नहीं है। LOCKउपसर्ग एक निर्देश को सही मायने में परमाणु बनाने के लिए आवश्यक है। ओपी बस भाग्यशाली हो रहा है। याद रखें कि भले ही आप सीपीयू को "इस पते पर शब्द में 1 जोड़ दें" कह रहे हों, सीपीयू को अभी भी मूल्य प्राप्त करना है, बढ़ाना है, उस मूल्य को स्टोर करना है और दूसरा सीपीयू एक साथ एक ही काम कर सकता है, जिससे परिणाम गलत हो सकता है।
जोनाथन रेइनहर्ट

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

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

3
ओह, निश्चित रूप से। x86 में पीछे की ओर संगतता का टन है ... यह सुनिश्चित करने के लिए सामान कि गलत तरीके से लिखा गया कोड संभव हद तक काम करता है। यह एक बहुत बड़ी बात थी जब पेंटियम प्रो ने आउट-ऑफ-ऑर्डर निष्पादन की शुरुआत की। इंटेल यह सुनिश्चित करना चाहता था कि कोड के स्थापित आधार को विशेष रूप से उनकी नई चिप के लिए पुन: संकलित किए जाने की आवश्यकता के बिना काम किया जाए। x86 एक CISC कोर के रूप में शुरू हुआ, लेकिन आंतरिक रूप से एक RISC कोर में विकसित हुआ है, हालांकि यह अभी भी एक प्रोग्रामर के नजरिए से CISC के रूप में कई तरह से प्रस्तुत करता है और व्यवहार करता है। अधिक के लिए, पीटर कॉर्डेस का जवाब यहां देखें
कोड़ी ग्रे

20

मुझे लगता है कि अगर आप पहले या बाद में नींद लेते हैं तो यह बहुत ज्यादा नहीं है u++। इसके बजाय यह है कि ऑपरेशन उस u++कोड का अनुवाद करता है जो कि है - स्पॉइंग थ्रेड्स के ओवरहेड की तुलना में जो कॉल करते हैं foo- बहुत तेज़ी से ऐसा प्रदर्शन किया जाता है जिससे इंटरसेप्ट होने की संभावना नहीं है। हालांकि, यदि आप ऑपरेशन को "लम्बा" करते हैं u++, तो दौड़ की स्थिति बहुत अधिक हो जाएगी:

void foo()
{
    unsigned i = u;
    for (int s=0;s<10000;s++);
    u = i+1;
}

परिणाम: 694


BTW: मैंने भी कोशिश की

if (u % 2) {
    u += 2;
} else {
    u -= 1;
}

और इसने मुझे सबसे अधिक बार दिया 1997, लेकिन कभी-कभी 1995


1
मैं किसी भी अस्पष्ट समझदार कंपाइलर पर उम्मीद करूंगा कि पूरे फ़ंक्शन को एक ही चीज़ के लिए अनुकूलित किया जाएगा। मुझे आश्चर्य है कि यह नहीं था। दिलचस्प परिणाम के लिए धन्यवाद।
२०

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

@Vality: मैंने यह भी उम्मीद की थी कि O3 ऑप्टिमाइज़ेशन के तहत स्पुरियस फॉर-लूप को हटा दिया जाए। यह नहीं है?
user21820

else u -= 1कभी भी कैसे अंजाम दिया जा सकता है? यहां तक ​​कि एक समानांतर वातावरण में मूल्य कभी भी फिट नहीं होना चाहिए %2, है ना?
माफ़ू

2
आउटपुट से, ऐसा लगता है कि इसे else u -= 1एक बार निष्पादित किया गया है, पहली बार फू () कहा जाता है, जब यू == 0. शेष 999 बार यू विषम होता है और u += 2इसे निष्पादित किया जाता है जिसके परिणामस्वरूप यू = -1 + 999 * 2 = 1997; यानी सही आउटपुट। एक दौड़ की स्थिति कभी-कभी + = 2 में से एक को एक समानांतर धागे द्वारा अधिलेखित कर देती है और आपको 1995 मिलती है।
लूका

7

यह एक दौड़ की स्थिति से ग्रस्त है। usleep(1000);पहले डाल u++;दिया fooऔर मैं हर बार अलग-अलग आउटपुट (<1000) देखता हूं।


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

  2. आपके मूल संस्करण के साथ भी, परिणाम सिस्टम से भिन्न होता है: मैंने इसे एक (क्वाड-कोर) मैकबुक पर अपना तरीका आज़माया, और दस रनों में, मुझे 1000 तीन बार, 999 छह बार, और 998 एक बार मिला। तो दौड़ कुछ दुर्लभ है, लेकिन स्पष्ट रूप से मौजूद है।

  3. आपने संकलित किया '-g' , जिसमें बग को गायब करने का एक तरीका है। मैंने आपके कोड को फिर से जोड़ दिया, फिर भी अपरिवर्तित रहा, लेकिन इसके बिना '-g', और दौड़ अधिक स्पष्ट हो गई: मुझे 1000 एक बार, 999 तीन बार, 998 दो बार, 997 दो बार, 996 एक बार, और एक बार 992 मिले।

  4. पुन। एक नींद जोड़ने का सुझाव - जो मदद करता है, लेकिन (ए) एक निश्चित नींद का समय शुरू होने के समय (समय के लिए संकल्प) के अनुसार थ्रेड्स को छोड़ देता है, और (बी) एक यादृच्छिक नींद उन्हें फैला देती है जब हम चाहते हैं। उन्हें एक साथ खींचो। इसके बजाय, मैं उन्हें एक स्टार्ट सिग्नल की प्रतीक्षा करने के लिए कोड देता हूं, इसलिए मैं उन्हें काम करने देने से पहले उन सभी को बना सकता हूं। इस संस्करण के साथ (साथ या बिना '-g'), मुझे हर जगह परिणाम मिलते हैं, 974 के रूप में कम, और 998 से अधिक नहीं:

    #include <iostream>
    #include <thread>
    #include <vector>
    using namespace std;
    
    unsigned u = 0;
    bool start = false;
    
    void foo()
    {
        while (!start) {
            std::this_thread::yield();
        }
        u++;
    }
    
    int main()
    {
        vector<thread> threads;
        for(int i = 0; i < 1000; i++) {
            threads.push_back (thread (foo));
        }
        start = true;
        for (auto& t : threads) t.join();
    
        cout << u << endl;
        return 0;
    }

सिर्फ एक नोट। -gझंडा किसी भी तरह से नहीं है "मेक कीड़े गायब हो जाते हैं।" -gदोनों जीएनयू और बजना compilers पर झंडा बस संकलित बाइनरी डिबग प्रतीकों कहते हैं। यह आपको कुछ मानवीय पठनीय आउटपुट के साथ GDB और मेमचेक जैसे नैदानिक ​​उपकरण चलाने की अनुमति देता है। उदाहरण के लिए जब मेमेकच को मेमोरी लीक के साथ एक प्रोग्राम पर चलाया जाता है तो यह आपको लाइन नंबर नहीं बताएगा जब तक कि -gध्वज का उपयोग करके प्रोग्राम नहीं बनाया गया था ।
MS-DDOS

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

-gआपको अनुकूलन का उपयोग करने से नहीं रोकता है। उदाहरण के gcc -O3 -gरूप में एक ही asm बनाता है gcc -O3, लेकिन डीबग मेटाडेटा के साथ। अगर आप कुछ वैरिएबल को प्रिंट करने की कोशिश करेंगे तो जीडीबी "ऑप्टिमाइज़ आउट" कहेगी। -gशायद कुछ चीजों के सापेक्ष स्थानों को स्मृति में बदल सकता है, अगर इसमें जो भी सामान शामिल है वह .textअनुभाग का हिस्सा है । यह निश्चित रूप से ऑब्जेक्ट फ़ाइल में जगह लेता है, लेकिन मुझे लगता है कि लिंक करने के बाद यह सब टेक्स्ट सेगमेंट (खंड नहीं) के एक छोर पर समाप्त होता है, या एक सेगमेंट का हिस्सा बिल्कुल नहीं। शायद यह प्रभावित कर सकता है कि गतिशील पुस्तकालयों के लिए चीजें कहां मैप की गई हैं।
पीटर कॉर्ड्स
हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.