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




race-condition (4)

मैं 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;
}

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

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

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

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

    #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;
    }

मुझे लगता है कि यह इतनी बात नहीं है अगर आप 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


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

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

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

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

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

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

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


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





race-condition