2013-01-24 37 views
9

मैं निम्नलिखित सी ++ 2011 कोड है:सी ++ memory_order/जारी

std::atomic<bool> x, y; 
std::atomic<int> z; 

void f() { 
    x.store(true, std::memory_order_relaxed); 
    std::atomic_thread_fence(std::memory_order_release); 
    y.store(true, std::memory_order_relaxed); 
} 

void g() { 
    while (!y.load(std::memory_order_relaxed)) {} 
    std::atomic_thread_fence(std::memory_order_acquire); 
    if (x.load(std::memory_order_relaxed)) ++z; 
} 

int main() { 
    x = false; 
    y = false; 
    z = 0; 
    std::thread t1(f); 
    std::thread t2(g); 
    t1.join(); 
    t2.join(); 
    assert(z.load() !=0); 
    return 0; 
} 

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

जो मैं जानता हूँ के लिए:

  • के बाद यह
  • साथ 'memory_order_acquire' ए बाड़ किसी भी लोड है कि अनुमति नहीं दी जाएगी पिछले भंडार निष्पादित करने की अनुमति नहीं दी जाएगी साथ 'memory_order_release' ए बाड़ इससे पहले इसे निष्पादित करने के बाद आता है।

यदि मेरी समझ सही है, तो निम्नलिखित अनुक्रमों का अनुक्रम क्यों नहीं हो सकता है?

  1. t1 अंदर, y.store(true, std::memory_order_relaxed);
  2. t2 पूरी तरह से चलाता है कहा जाता है, और जब 'एक्स' लोड हो रहा है, इसलिए एक इकाई में जेड बढ़ती नहीं एक 'गलत' देखेंगे
  3. t1 खत्म निष्पादन
  4. में मुख्य थ्रेड, ज़ोर विफल रहता है क्योंकि z.load() रिटर्न 0

मैं इस 'अधिग्रहण' के अनुरूप है लगता है - 'रिलीज' नियम है, लेकिन, इस सवाल में सबसे अच्छा जवाब में उदाहरण के लिए: Understanding c++11 memory fences जो बहुत है SIMI मेरे मामले में लार, यह संकेत देता है कि मेरे अनुक्रम में चरण 1 की तरह कुछ 'memory_order_release' से पहले नहीं हो सकता है, लेकिन इसके पीछे कारण के लिए विवरण नहीं मिलता है।

मैं इस बारे में बहुत हैरान हूँ, और बहुत खुशी होगी अगर कोई कुछ प्रकाश डाला सकता है पर यह :)

उत्तर

4

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

तो यह मानते हुए एक हाथ या इसी तरह के प्रोसेसर के साथ जो अपने आप में कैश-सुसंगत होने की गारंटी नहीं है पर चल रहा है:

क्योंकि x को लिखने memory_order_release से पहले किया जाता है, t2 पाश से बाहर निकलने नहीं होंगे while(y...)x तक भी सच है। इसका मतलब है कि जब x बाद में पढ़ा जा रहा है, तो यह एक होने की गारंटी है, इसलिए z अपडेट किया गया है। मेरी केवल मामूली क्वेरी के लिए यदि आप z के लिए एक release रूप में अच्छी तरह की जरूरत नहीं है ... तो maint1 और t2 तुलना में एक अलग प्रोसेसर पर चल रहा है के रूप में, तो z stil main में एक बासी मूल्य हो सकता है।

बेशक, यदि आपके पास मल्टीटास्किंग ओएस (या केवल पर्याप्त सामान, जो पर्याप्त सामान करता है, इत्यादि) है तो ऐसा होने के लिए गारंटी नहीं है - क्योंकि अगर टी 1 चलाने वाले प्रोसेसर को कैश फ्लश हो जाता है, तो टी 2 अच्छी तरह से नया मान पढ़ सकता है एक्स का

और जैसा कि मैंने कहा, इसका प्रभाव x86 प्रोसेसर (एएमडी या इंटेल वाले) पर नहीं होगा।

तो, सामान्य रूप में बाधा निर्देश (यह भी इंटेल और एएमडी process0rs के लिए लागू) की व्याख्या:

सबसे पहले, हम समझने के लिए एक सामान्य "है कि हालांकि निर्देश शुरू करने और आदेश से बाहर समाप्त कर सकते हैं, प्रोसेसर है जरूरत आदेश की समझ "।

... 
mov $5, x 
cmp a, b 
jnz L1 
mov $4, x 

एल 1: मान लीजिए कि हम इस "छद्म मशीन कोड" करते हैं ...

प्रोसेसर अनुमान के आधार पर निष्पादित कर सकता है mov $4, x इससे पहले कि यह "JNZ एल 1" पूरा - तो, ​​इस को हल करने के वास्तव में, प्रोसेसर को mov $4, x को उस मामले में रोल-बैक करना होगा जहां jnz L1 लिया गया था।

इसी तरह, अगर हमने:

mov $1, x 
wmb   // "write memory barrier" 
mov $1, y 

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

समान रूप से, "रीड मेमोरी बाधा" एक निर्देश है जो प्रोसेसर के डिजाइनरों द्वारा गारंटी देता है, कि प्रोसेसर एक और पढ़ा नहीं जाएगा जब तक कि हम बाधा निर्देश से पहले लंबित पढ़ाई पूरी नहीं कर लेते।

जब तक हम "प्रयोगात्मक" प्रोसेसर या कुछ स्कैकी चिप पर काम नहीं कर रहे हैं जो सही ढंग से काम नहीं करता है, तो यह इस तरह से काम करेगा। यह उस निर्देश की परिभाषा का हिस्सा है। ऐसी गारंटी के बिना, यह (सुरक्षित) स्पिनलॉक्स, सेमफोर, म्यूटेक्स इत्यादि को लागू करने के लिए असंभव (या कम से कम बेहद जटिल और "महंगा") होगा

अक्सर "निहित स्मृति बाधाएं" भी होती है - यानी, निर्देश जो मेमोरी बाधाओं का कारण बनता है भले ही वे नहीं हैं। सॉफ्टवेयर इंटरप्ट्स ("आईएनटी एक्स" निर्देश या इसी तरह) ऐसा करते हैं।

+0

+1। लेकिन सवाल: क्या आप कह रहे हैं कि एक गैर-कैश-सुसंगत वास्तुकला पर, 'memory_order_release'-स्मृति बाड़ भी अन्य प्रोसेसर के कैश अपडेट होने के लिए पर्याप्त नहीं है? – jogojapan

+0

नहीं, मैं कह रहा हूं कि 'memory_order_release' का उपयोग करके कैश-कोहेरेंसी सुनिश्चित करेगा। लेकिन मुझे माफी मांगनी है, मैंने आपका कोड गलत पढ़ा है - मुझे नहीं लगता कि यह दावा कैसे विफल करेगा। यदि y t2 में सत्य है, तो x t2 में सत्य होगा। तो, मान लीजिए कि 't2' लूप अंततः खत्म हो जाता है, फिर 'z' को बढ़ाया जाना चाहिए। उसके लिए माफ़ करना। –

+0

मैं उत्तर अपडेट कर दूंगा। लेकिन ध्यान रखें कि दो "शामिल" यह सुनिश्चित करता है कि मुख्य रूप से जोर देने से पहले दोनों धागे समाप्त हो जाएं। –

2

मुझे यह प्रोसेसर ऐसा करने के मामले में सी ++ समवर्ती प्रश्नों के बारे में बहस करना पसंद नहीं है, वह प्रोसेसर ऐसा करता है "। सी ++ 11 में एक मेमोरी मॉडल है, और हमें यह निर्धारित करने के लिए इस मेमोरी मॉडल का उपयोग करना चाहिए कि क्या वैध है और क्या नहीं है। सीपीयू आर्किटेक्चर और मेमोरी मॉडल आमतौर पर समझने के लिए कठिन होते हैं। इसके अलावा उनमें से एक से अधिक है।

इस बात को ध्यान में रखते हुए, इस पर विचार करें: थ्रेड टी 2 को लूप में अवरुद्ध कर दिया जाता है जब तक टी 1 y.store निष्पादित नहीं करता है और परिवर्तन t2 तक फैल गया है। (जो, वैसे, सिद्धांत में कभी नहीं हो सकता है। लेकिन यह यथार्थवादी नहीं है।) इसलिए हमारे पास टी 1 में y.store के बीच संबंध और टी 2 में y.load के बीच संबंध होता है जो इसे लूप छोड़ने की अनुमति देता है।

इसके अलावा, हमारे पास x.store और रिलीज बाधा और अवरोध और y.store के बीच संबंधों से पहले सरल अंतर-धागा होता है।

टी 2 में, हमारे पास वास्तविक-वापसी भार और अधिग्रहण बाधा और x.load के बीच एक होता है।

क्योंकि होता है-पहले संक्रमणीय होता है, रिलीज बाधा होता है-अधिग्रहण बाधा से पहले, और x.store x.load से पहले होता है। बाधाओं के कारण, x.store x के साथ सिंक्रनाइज़ करता है।लोड, जिसका मतलब है कि लोड को संग्रहीत मूल्य देखना है।

अंत में, z.add_and_fetch (पोस्ट-वृद्धि) होता है-थ्रेड समाप्ति से पहले होता है, जो होता है- मुख्य थ्रेड t2.join से निकलता है, जो होता है- मुख्य थ्रेड में z.load से पहले होता है, इसलिए जेड में संशोधन मुख्य धागे में दिखाई देना चाहिए। रोचक जानकारी के लिए

+0

हां, तो नीचे की रेखा यह है कि जो मुझे समझ में नहीं आ रहा था वह यह है कि 'std :: atomic_thread_fence (std :: memory_order_release);' पिछले स्टोरों के बीच एक पूर्व-संबंध संबंध बनाता है, और 'y.store (सच, std :: memory_order_relaxed); '। दुर्भाग्यवश मैट्स ने पहले ही इसे समझाया है इसलिए मैं उसे त्वरितता के लिए चेकमार्क देता हूं :) – alfongj