मल्टीथ्रेडिंग प्रोग्राम अनुकूलित मोड में अटका हुआ है, लेकिन सामान्य रूप से -O0 में चलता है


68

मैंने एक सरल मल्टीथ्रेडिंग कार्यक्रम इस प्रकार लिखा है:

static bool finished = false;

int func()
{
    size_t i = 0;
    while (!finished)
        ++i;
    return i;
}

int main()
{
    auto result=std::async(std::launch::async, func);
    std::this_thread::sleep_for(std::chrono::seconds(1));
    finished=true;
    std::cout<<"result ="<<result.get();
    std::cout<<"\nmain thread id="<<std::this_thread::get_id()<<std::endl;
}

यह में डिबग मोड में सामान्य रूप से बर्ताव करता है दृश्य स्टूडियो या -O0में जीसी ग और परिणाम प्रिंट आउट के बाद 1सेकंड। लेकिन यह अटक गया और रिलीज मोड में या कुछ भी प्रिंट नहीं करता है -O1 -O2 -O3


टिप्पणियाँ विस्तारित चर्चा के लिए नहीं हैं; इस वार्तालाप को बातचीत में स्थानांतरित कर दिया गया है ।
शमूएल एलवाई

जवाबों:


100

गैर-परमाणु, गैर-संरक्षित चर तक पहुंचने वाले दो सूत्र, यूबी यह चिंताएं हैं finished। आप इसे ठीक करने finishedके लिए प्रकार बना सकते हैं std::atomic<bool>

मेरा फिक्स:

#include <iostream>
#include <future>
#include <atomic>

static std::atomic<bool> finished = false;

int func()
{
    size_t i = 0;
    while (!finished)
        ++i;
    return i;
}

int main()
{
    auto result=std::async(std::launch::async, func);
    std::this_thread::sleep_for(std::chrono::seconds(1));
    finished=true;
    std::cout<<"result ="<<result.get();
    std::cout<<"\nmain thread id="<<std::this_thread::get_id()<<std::endl;
}

आउटपुट:

result =1023045342
main thread id=140147660588864

कोलिरु पर लाइव डेमो


कोई सोच सकता है 'यह एक bool- शायद एक सा है। यह गैर-परमाणु कैसे हो सकता है? ' (मैंने तब किया जब मैंने खुद को मल्टी-थ्रेडिंग के साथ शुरू किया।)

लेकिन ध्यान दें कि कमी-आंसू ही एकमात्र चीज नहीं है std::atomicजो आपको देती है। यह कई थ्रेड्स से समवर्ती रीड + राइट एक्सेस को भी अच्छी तरह से परिभाषित करता है, जिससे कंपाइलर को यह मानने से रोक दिया जाता है कि चर को फिर से पढ़ना हमेशा एक ही मूल्य दिखाई देगा।

boolगैर-परमाणु, गैर-परमाणु बनाने से अतिरिक्त समस्याएं हो सकती हैं:

  • कंपाइलर चर को एक रजिस्टर में या यहां तक ​​कि CSE के कई एक्सेस को एक में ऑप्टिमाइज़ करने और लूप से लोड को बाहर निकालने का निर्णय ले सकता है।
  • चर को सीपीयू कोर के लिए कैश किया जा सकता है। (वास्तविक जीवन में, सीपीयू में सुसंगत कैश होते हैं । यह एक वास्तविक समस्या नहीं है, लेकिन सी ++ मानक गैर-सुसंगत साझा मेमोरी पर काल्पनिक सी ++ कार्यान्वयन को कवर करने के लिए पर्याप्त ढीला है जहां स्टोर / लोड के atomic<bool>साथ memory_order_relaxedकाम होगा, लेकिन जहां volatileउपयोग नहीं होगा) इसके लिए वाष्पशील यूबी होगा, भले ही यह वास्तविक सी ++ कार्यान्वयन पर अभ्यास में काम करता है।)

ऐसा होने से रोकने के लिए, कंपाइलर को स्पष्ट रूप से ऐसा नहीं करने के लिए कहा जाना चाहिए।


मैं volatileइस मुद्दे के संभावित संबंध के बारे में विकसित चर्चा के बारे में थोड़ा हैरान हूं। इस प्रकार, मैं अपने दो सेंट खर्च करना चाहूंगा:


4
मैंने एक नज़र func()डाली और सोचा कि "मैं इसे दूर कर सकता हूं" आशावादी बिल्कुल भी धागे की परवाह नहीं करता है, और अनंत लूप का पता लगाएगा, और ख़ुशी से इसे "जबकि (सच्चा)" में बदल देगा यदि हम गॉडबोल्ट को देखते हैं .org / z / Tl44iN हम इसे देख सकते हैं। यदि समाप्त हो गया है तो Trueयह रिटर्न है। यदि ऐसा नहीं है, तो यह लेबल पर स्वयं (एक अनंत लूप) में बिना शर्त कूद जाता है.L5
बाल्क्रीक


2
@val: मूल रूप volatileसे C ++ 11 में दुरुपयोग का कोई कारण नहीं है क्योंकि आप के साथ समान रूप से प्राप्त कर सकते हैं atomic<T>और std::memory_order_relaxed। हालांकि यह वास्तविक हार्डवेयर पर काम करता है: कैश सुसंगत हैं इसलिए एक लोड निर्देश एक बार दूसरे कैश पर स्टोर करने के लिए बासी मूल्य नहीं पढ़ सकता है। (MESI)
पीटर कॉर्ड्स

5
@PeterCordes volatileयूबी हालांकि अभी भी यूबी है। आपको वास्तव में कभी भी कुछ ऐसा नहीं मान लेना चाहिए जो निश्चित रूप से और स्पष्ट रूप से यूबी सुरक्षित है क्योंकि आप एक तरह से सोच भी नहीं सकते कि यह गलत हो सकता है और जब आप इसे आज़माते हैं तो यह काम करता है। इसने लोगों को बार-बार जला दिया है।
डेविड श्वार्ट्ज 5

2
@Damon Mutexes के पास शब्दार्थ / विमोचन है। कंपाइलर को पढ़ने के अनुकूलन की अनुमति नहीं है यदि म्यूटेक्स को पहले बंद किया गया था, इसलिए finishedएक std::mutexकार्य (बिना volatileया atomic) के साथ रक्षा करना । वास्तव में, आप सभी परमाणुओं को "सरल" मान + म्यूटेक्स योजना से बदल सकते हैं; यह अभी भी काम करेगा और बस धीमा होगा। atomic<T>एक आंतरिक म्यूटेक्स का उपयोग करने की अनुमति है; केवल atomic_flagलॉक-फ्री की गारंटी है।
Erlkoenig

42

Scheff का उत्तर बताता है कि आपके कोड को कैसे ठीक किया जाए। मैंने सोचा कि इस मामले में वास्तव में क्या हो रहा है, इस पर मैं थोड़ी जानकारी जोड़ूंगा।

मैंने अनुकूलन स्तर 1 ( ) का उपयोग करके आपके कोड को गॉडबॉल्ट में संकलित किया -O1। आपका फ़ंक्शन इस तरह संकलित करता है:

func():
  cmp BYTE PTR finished[rip], 0
  jne .L4
.L5:
  jmp .L5
.L4:
  mov eax, 0
  ret

तो, यहाँ क्या हो रहा है? सबसे पहले, हमारे पास एक तुलना है: cmp BYTE PTR finished[rip], 0- यह देखने के लिए कि क्या हैfinished क्या झूठा है या नहीं ।

यदि यह गलत नहीं है (उर्फ सच) हमें पहले रन पर लूप से बाहर निकलना चाहिए। इस के द्वारा पूरा किया jne .L4जो जे umps जब n OT लेबल से qual .L4जहां का मूल्य i(0 ) बाद में उपयोग और समारोह रिटर्न के लिए एक रजिस्टर में संग्रहित है।

यदि यह है तथापि झूठी है, हम करने के लिए ले जाने के

.L5:
  jmp .L5

यह एक बिना शर्त कूद है, लेबल करने के लिए .L5 जो सिर्फ इतना ही होता है कि जंप कमांड खुद हो।

दूसरे शब्दों में, धागे को एक अनंत व्यस्त लूप में डाल दिया जाता है।

तो ऐसा क्यों हुआ है?

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

क्योंकि ऑप्टिमाइज़र को यह नहीं बताया जाता है कि finishedफ़ंक्शन के निष्पादन के दौरान चर संभावित रूप से बदल सकता है, यह देखता है कि finishedफ़ंक्शन द्वारा ही संशोधित नहीं किया गया है और मानता है कि यह निरंतर है।

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

पर -O0संकलक (उम्मीद के रूप में) पाश शरीर और तुलना दूर अनुकूलन नहीं करता:

func():
  push rbp
  mov rbp, rsp
  mov QWORD PTR [rbp-8], 0
.L148:
  movzx eax, BYTE PTR finished[rip]
  test al, al
  jne .L147
  add QWORD PTR [rbp-8], 1
  jmp .L148
.L147:
  mov rax, QWORD PTR [rbp-8]
  pop rbp
  ret

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

डेटा-संरचनाओं के साथ एक अधिक जटिल प्रणाली के परिणामस्वरूप दूषित डेटा, या अनुचित निष्पादन की संभावना अधिक होती है।


3
C ++ 11 थ्रेड्स और थ्रेड-जागरूक मेमोरी मॉडल को भाषा का ही हिस्सा बनाता है। इसका मतलब यह है कि संकलक उन कोडों atomicमें न लिखने वाले वेरिएबल्स के लिए भी आविष्कार नहीं कर सकते हैं जो उन वेरिएबल्स को नहीं लिखते हैं। उदाहरण के लिए if (cond) foo=1;इसे asm में बदला नहीं जा सकता foo = cond ? 1 : foo;क्योंकि यह है कि लोड + स्टोर (एक परमाणु RMW नहीं) दूसरे धागे से एक लेख पर कदम रख सकता है। कंपाइलर पहले से ही इस तरह के सामान से बच रहे थे क्योंकि वे बहु-थ्रेडेड प्रोग्राम लिखने के लिए उपयोगी होना चाहते थे, लेकिन C ++ 11 ने इसे आधिकारिक बना दिया कि कंपाइलरों को कोड को नहीं तोड़ना था जहां 2 धागे लिखते हैं a[1]औरa[2]
पीटर कॉर्ड्स

2
लेकिन हाँ, के बारे में कैसे compilers धागे की जानकारी नहीं है कि overstatement के अलावा अन्य सभी पर , आपका जवाब सही है। डेटा-रेस यूबी वह है जो ग्लोबल्स सहित गैर-परमाणु चर का भार उठाने की अनुमति देता है, और अन्य आक्रामक अनुकूलन जो हम एकल-थ्रेडेड कोड के लिए चाहते हैं। MCU प्रोग्रामिंग - इलेक्ट्रॉनिक्स पर लूप करते समय C ++ O2 ऑप्टिमाइज़ेशन टूट जाता है। इस स्पष्टीकरण का मेरा संस्करण है।
पीटर कॉर्ड्स

1
@PeterCordes: एक GC का उपयोग करके जावा का एक फायदा यह है कि वस्तुओं के लिए मेमोरी को पुराने और नए उपयोग के बीच एक हस्तक्षेप वैश्विक मेमोरी बाधा के बिना पुनर्नवीनीकरण नहीं किया जाएगा , जिसका अर्थ है कि किसी भी वस्तु की जांच करने वाले किसी भी कोर को हमेशा कुछ मूल्य दिखाई देगा कि यह संदर्भ प्रकाशित होने के कुछ समय बाद आयोजित किया गया था। जबकि वैश्विक मेमोरी बैरियर बहुत महंगे हो सकते हैं यदि उनका अक्सर उपयोग किया जाता है, तो वे संयमित रूप से उपयोग किए जाने पर भी अन्य जगहों पर मेमोरी बाधाओं की आवश्यकता को बहुत कम कर सकते हैं।
सुपरकैट

1
हां, मुझे पता था कि आप क्या कहना चाह रहे थे, लेकिन मुझे नहीं लगता कि आपका शब्दांकन 100% है। आशावादी कहना "पूरी तरह से उन्हें अनदेखा करता है।" यह बिलकुल सही नहीं है: यह सर्वविदित है कि अनुकूलन को अनदेखा करते समय थ्रेडिंग को अनदेखा करना शब्द लोड जैसी चीजों को शामिल कर सकता है / शब्द / शब्द भंडार में एक बाइट को संशोधित कर सकता है, जिसके कारण व्यवहार में बग पैदा हो जाते हैं, जहां एक थ्रेड की पहुंच एक char या bitfield कदमों पर होती है। आसन्न संरचना सदस्य को लिखें। पूरी कहानी के लिए lwn.net/Articles/478657 देखें , और केवल C11 / C ++ 11 मेमोरी मॉडल इस तरह के अनुकूलन को अवैध बनाता है, न कि केवल व्यवहार में अवांछित।
पीटर कॉर्ड्स

1
नहीं, यह अच्छा है .. धन्यवाद @PeterCordes मैं सुधार की सराहना करता हूं।
बाल्ड्रिक

5

सीखने की अवस्था में पूर्णता के लिए; आपको वैश्विक चर का उपयोग करने से बचना चाहिए। आपने इसे स्थिर बनाकर अच्छा काम किया, इसलिए यह अनुवाद इकाई के लिए स्थानीय होगा।

यहाँ एक उदाहरण है:

class ST {
public:
    int func()
    {
        size_t i = 0;
        while (!finished)
            ++i;
        return i;
    }
    void setFinished(bool val)
    {
        finished = val;
    }
private:
    std::atomic<bool> finished = false;
};

int main()
{
    ST st;
    auto result=std::async(std::launch::async, &ST::func, std::ref(st));
    std::this_thread::sleep_for(std::chrono::seconds(1));
    st.setFinished(true);
    std::cout<<"result ="<<result.get();
    std::cout<<"\nmain thread id="<<std::this_thread::get_id()<<std::endl;
}

वैंडबॉक्स पर लाइव


1
यह भी घोषणा कर सकते हैं finishedके रूप में staticसमारोह ब्लॉक के भीतर। यह अभी भी केवल एक बार आरंभीकृत किया जाएगा, और यदि इसे किसी स्थिरांक पर आरंभीकृत किया जाता है, तो इसके लिए लॉकिंग की आवश्यकता नहीं है।
डेविसलर

finishedसस्ता std::memory_order_relaxedलोड और स्टोर का उपयोग करने के लिए एक्सेस भी हो सकता है ; कोई आवश्यक आदेश wrt नहीं है। किसी भी धागे में अन्य चर। मुझे यकीन नहीं है @ डेविसोर का सुझाव staticसमझ में आता है, हालांकि; यदि आपके पास कई स्पिन-गिनती धागे हैं, तो आप उन सभी को एक ही ध्वज के साथ रोकना नहीं चाहेंगे। आप finishedएक तरह से इनिशियलाइज़ेशन लिखना चाहते हैं जो केवल इनिशियलाइज़ेशन के लिए संकलित हो, न कि एटॉमिक स्टोर, हालाँकि। (जैसे आप finished = false;डिफ़ॉल्ट इनिशियलाइज़र C ++ 17 सिंटैक्स के साथ कर रहे हैं । godbolt.org/z/EjoKgq )।
पीटर कॉर्डेस

@PeterCordes ध्वज को किसी ऑब्जेक्ट में रखने से अलग-अलग थ्रेड पूल के लिए एक से अधिक होने की अनुमति मिलती है, जैसा कि आप कहते हैं। मूल डिजाइन में सभी धागों के लिए एक ही ध्वज था।
डेविसलर
हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.