मैं पोर्टेबल कोड (इंटेल, एआरएम, पावरपीसी ...) लिखना चाहता हूं जो एक क्लासिक समस्या का एक प्रकार हल करता है:
Initially: X=Y=0
Thread A:
X=1
if(!Y){ do something }
Thread B:
Y=1
if(!X){ do something }
जिसमें लक्ष्य एक ऐसी स्थिति से बचने का है जिसमें दोनों धागे कर रहे हैंsomething
। (यह ठीक है अगर कोई बात नहीं चलती है, यह एक रन-बिल्कुल-एक बार तंत्र नहीं है।) कृपया मुझे ठीक करें यदि आपको नीचे मेरे तर्क में कुछ खामियां दिखाई देती हैं।
मुझे पता है, कि मैं memory_order_seq_cst
परमाणु के साथ लक्ष्य प्राप्त कर सकता हूं store
और load
निम्नानुसार है:
std::atomic<int> x{0},y{0};
void thread_a(){
x.store(1);
if(!y.load()) foo();
}
void thread_b(){
y.store(1);
if(!x.load()) bar();
}
जो लक्ष्य को प्राप्त करता है, क्योंकि
{x.store(1), y.store(1), y.load(), x.load()}
घटनाओं पर कुछ एकल कुल आदेश होना चाहिए, जिसे प्रोग्राम ऑर्डर "किनारों" से सहमत होना चाहिए:
x.store(1)
"टू में पहले है"y.load()
y.store(1)
"टू में पहले है"x.load()
और अगर foo()
बुलाया गया था, तो हमारे पास अतिरिक्त बढ़त है:
y.load()
"मूल्य से पहले पढ़ता है"y.store(1)
और अगर bar()
बुलाया गया था, तो हमारे पास अतिरिक्त बढ़त है:
x.load()
"मूल्य से पहले पढ़ता है"x.store(1)
और इन सभी किनारों को मिलाकर एक चक्र बनेगा:
x.store(1)
"टू में पहले है" y.load()
" पहले पढ़ता है मूल्य" से पहले y.store(1)
"में है" x.load()
"पहले पढ़ता है मूल्य"x.store(true)
जो इस तथ्य का उल्लंघन करता है कि आदेशों का कोई चक्र नहीं है।
मैं जानबूझकर "से पहले है" और "मान से पहले मान" जैसे मानक शर्तों के विपरीत "पढ़ता है" का उपयोग करता happens-before
हूं, क्योंकि मैं अपनी धारणा की शुद्धता के बारे में प्रतिक्रिया देना चाहता हूं कि इन किनारों का वास्तव में happens-before
संबंध, एकल में एक साथ जोड़ा जा सकता है ग्राफ, और इस तरह के संयुक्त ग्राफ में चक्र निषिद्ध है। मैं उसके बारे में निश्चित नहीं हूं। मुझे पता है कि यह कोड इंटेल जीसीसी और क्लेंग और एआरएम जीसीसी पर सही अवरोध पैदा करता है
अब, मेरी वास्तविक समस्या थोड़ी अधिक जटिल है, क्योंकि "X" पर मेरा कोई नियंत्रण नहीं है - यह कुछ मैक्रोज़, टेम्प्लेट आदि के पीछे छिपा हुआ है और इससे कमज़ोर हो सकता है। seq_cst
मुझे यह भी नहीं पता है कि "X" एक एकल चर है, या कुछ अन्य अवधारणा (उदाहरण के लिए एक हल्के वजन वाला सेमाफोर या म्यूटेक्स)। मुझे पता है कि मेरे पास दो मैक्रोज़ हैं set()
और check()
ऐसे " check()
रिटर्न true
" के बाद "एक और धागा" कहा जाता है set()
। (यह है भी है कि जाना जाता है set
और check
धागे की सुरक्षित हैं और डेटा-दौड़ यूबी नहीं बना सकते।)
तो वैचारिक रूप set()
से कुछ हद तक "एक्स = 1" check()
जैसा है और "एक्स" जैसा है, लेकिन मेरे पास एटोमिक्स तक कोई सीधी पहुंच नहीं है, यदि कोई हो।
void thread_a(){
set();
if(!y.load()) foo();
}
void thread_b(){
y.store(1);
if(!check()) bar();
}
मुझे चिंता है, कि set()
आंतरिक रूप से लागू किया जा सकता है x.store(1,std::memory_order_release)
और / या check()
हो सकता है x.load(std::memory_order_acquire)
। या काल्पनिक रूप से std::mutex
कि एक धागा अनलॉक हो रहा है और दूसरा try_lock
आईएनजी है; आईएसओ मानक std::mutex
में केवल अधिग्रहण और रिलीज ऑर्डर देने की गारंटी है, seq_cst की नहीं।
यदि यह मामला है, तो check()
अगर शरीर पहले "फिर से व्यवस्थित" हो सकता है y.store(true)
( एलेक्स का जवाब देखें जहां वे प्रदर्शित करते हैं कि यह पावरपीसी पर होता है )।
यह वास्तव में बुरा होगा, क्योंकि अब घटनाओं का यह क्रम संभव है:
thread_b()
पहलेx
(0
) के पुराने मूल्य को लोड करता हैthread_a()
सहित सब कुछ निष्पादित करता हैfoo()
thread_b()
सहित सब कुछ निष्पादित करता हैbar()
तो, दोनों foo()
और bar()
बुला लिया गया है, जो मैं से बचने के लिए किया था। इसे रोकने के लिए मेरे पास क्या विकल्प हैं?
विकल्प ए
स्टोर-लोड बाधा को मजबूर करने की कोशिश करें। यह, व्यवहार में, द्वारा प्राप्त किया जा सकता है std::atomic_thread_fence(std::memory_order_seq_cst);
- जैसा कि एलेक्स द्वारा एक अलग उत्तर में समझाया गया है सभी परीक्षित संकलक एक पूर्ण बाड़ उत्सर्जित करते हैं:
- x86_64: MFENCE
- PowerPC: hwsync
- इतानुम: mf
- ARMv7 / ARMv8: dmb ish
- MIPS64: सिंक
इस दृष्टिकोण के साथ समस्या यह है, कि मुझे C ++ नियमों में कोई गारंटी नहीं मिली, कि std::atomic_thread_fence(std::memory_order_seq_cst)
पूर्ण मेमोरी बैरियर में अनुवाद करना होगा। दरअसल, atomic_thread_fence
C ++ में s की अवधारणा स्मृति अवरोधों की असेंबली अवधारणा की तुलना में एक अलग स्तर पर होती है और "परमाणु ऑपरेशन किस चीज़ के साथ सिंक्रनाइज़ होता है" जैसे सामानों के साथ अधिक व्यवहार करता है। क्या कोई सैद्धांतिक प्रमाण है कि नीचे कार्यान्वयन लक्ष्य को प्राप्त करता है?
void thread_a(){
set();
std::atomic_thread_fence(std::memory_order_seq_cst)
if(!y.load()) foo();
}
void thread_b(){
y.store(true);
std::atomic_thread_fence(std::memory_order_seq_cst)
if(!check()) bar();
}
विकल्प बी
नियंत्रण का उपयोग करें हमारे पास वाई पर तुल्यकालन प्राप्त करने के लिए, Y- पर पढ़ने-लिखने-लिखने memory_order_acq_rel संचालन का उपयोग करके:
void thread_a(){
set();
if(!y.fetch_add(0,std::memory_order_acq_rel)) foo();
}
void thread_b(){
y.exchange(1,std::memory_order_acq_rel);
if(!check()) bar();
}
यहाँ विचार यह है कि एकल परमाणु ( y
) तक पहुँच एक एकल क्रम होनी चाहिए जिस पर सभी पर्यवेक्षक सहमत हों, इसलिएfetch_add
पहले exchange
या इसके विपरीत है।
यदि fetch_add
पहले है, exchange
तो "रिलीज" भाग के fetch_add
"अधिग्रहण" भाग के साथ सिंक्रनाइज़ करता है exchange
और इस प्रकार सभी साइड इफेक्ट्स set()
कोडिंग के लिए दिखाई देते हैंcheck()
, इसलिए bar()
इसे नहीं बुलाया जाएगा।
अन्यथा, exchange
पहले है fetch_add
, फिर fetch_add
देखेंगे 1
और कॉल नहीं करेंगे foo()
। तो, दोनों को कॉल करना असंभव है foo()
औरbar()
। क्या यह तर्क सही है?
विकल्प सी
"किनारों" को शुरू करने के लिए डमी परमाणु का उपयोग करें, जो आपदा को रोकते हैं। निम्नलिखित दृष्टिकोण पर विचार करें:
void thread_a(){
std::atomic<int> dummy1{};
set();
dummy1.store(13);
if(!y.load()) foo();
}
void thread_b(){
std::atomic<int> dummy2{};
y.store(1);
dummy2.load();
if(!check()) bar();
}
अगर आपको लगता है कि यहाँ समस्या है atomic
स्थानीय है, तो उन्हें वैश्विक दायरे में ले जाने की कल्पना करें, निम्नलिखित कारणों से यह मेरे लिए मायने नहीं रखता है, और मैंने जानबूझकर कोड को इस तरह से उजागर करने के लिए लिखा है कि यह कितना हास्यास्पद है और डमी 2 पूरी तरह से अलग हैं।
पृथ्वी पर यह क्यों काम कर सकता है? वैसे, {dummy1.store(13), y.load(), y.store(1), dummy2.load()}
कार्यक्रम के "किनारों" के अनुरूप होने के लिए कुछ एकल कुल क्रम होना चाहिए :
dummy1.store(13)
"टू में पहले है"y.load()
y.store(1)
"टू में पहले है"dummy2.load()
(एक seq_cst स्टोर + लोड उम्मीद है कि स्टोरलॉड सहित पूर्ण मेमोरी बैरियर के C ++ के बराबर है, जैसे कि वे वास्तविक ISAs पर भी AArch64 सहित अलग-अलग हैं जहां कोई अलग बाधा निर्देश की आवश्यकता नहीं है।)
अब, हमारे पास विचार करने के लिए दो मामले हैं: या तो y.store(1)
पहले हैy.load()
या बाद में कुल आदेश है।
अगर y.store(1)
पहले है y.load()
तो foo()
नहीं बुलाया जाएगा और हम सुरक्षित हैं।
यदि y.load()
पहले है y.store(1)
, तो इसे दो किनारों के साथ जोड़कर हमारे पास पहले से ही कार्यक्रम क्रम में है, हम इसे घटाते हैं:
dummy1.store(13)
"टू में पहले है"dummy2.load()
अब, dummy1.store(13)
एक रिलीज ऑपरेशन है, जो के प्रभाव को जारी करता है set()
, और dummy2.load()
एक अधिग्रहण ऑपरेशन है, इसलिए check()
इसके प्रभावों को देखना चाहिए set()
और इस तरह bar()
नहीं बुलाया जाएगा और हम सुरक्षित हैं।
क्या यह सोचना सही है कि check()
इसके परिणाम देखने को मिलेंगे set()
? क्या मैं विभिन्न प्रकारों के "किनारों" ("प्रोग्राम ऑर्डर" उर्फ अनुक्रम से पहले, "कुल ऑर्डर", "रिलीज से पहले", "अधिग्रहण के बाद") को जोड़ सकता हूं? मुझे इस बारे में गंभीर संदेह है: सी ++ के नियमों के बारे में बात करने लगते हैं "सिंक्रनाइज़-इन" स्टोर और लोड के बीच एक ही स्थान पर संबंध - यहां ऐसी कोई स्थिति नहीं है।
ध्यान दें कि हम केवल इस मामले में जहां के बारे में चिंतित रहे dumm1.store
है जाना जाता है (अन्य तर्क के माध्यम से) से पहले होने की dummy2.load
seq_cst कुल आदेश में। इसलिए यदि वे एक ही चर पर पहुंच रहे थे, तो लोड ने संग्रहीत मूल्य को देखा होगा और इसके साथ सिंक्रनाइज़ किया गया था।
(मेमोरी-बैरियर / रीऑर्डरिंग कार्यान्वयन के लिए तर्क जहां परमाणु भार और स्टोर कम से कम 1-रास्ता मेमोरी बैरियर के लिए संकलित करते हैं (और seq_cst ऑपरेशंस को फिर से व्यवस्थित नहीं कर सकते हैं: उदाहरण के लिए seq_cst स्टोर seq_cst लोड पास नहीं कर सकता है) यह है कि कोई भी लोड / दुकानों के बाद dummy2.load
निश्चित रूप से अन्य धागे के बाद दिखाई देते हैं y.store
। और इसी तरह दूसरे धागे के लिए, ... पहले y.load
।)
आप https://godbolt.org/z/u3dTa8 पर विकल्प A, B, C के मेरे कार्यान्वयन के साथ खेल सकते हैं
foo()
और रोकें bar()
।
compare_exchange_*
एक परमाणु बूल पर आरएमडब्ल्यू ऑपरेशन का उपयोग उसके मूल्य को बदलने के बिना कर सकते हैं (बस उसी मूल्य पर अपेक्षित और नया सेट करें)।
atomic<bool>
है exchange
और compare_exchange_weak
। उत्तरार्द्ध का उपयोग कैस (सच्चा, सच्चा) या असत्य, असत्य का प्रयास करके डमी आरएमडब्ल्यू करने के लिए किया जा सकता है। यह या तो विफल हो जाता है या परमाणु मूल्य को स्वयं के साथ बदल देता है। (X86-64 asm में, वह ट्रिक lock cmpxchg16b
यह है कि आप गारंटीकृत-परमाणु 16-बाइट लोड कैसे करते हैं, एक अलग ताला लेने की तुलना में अक्षम लेकिन कम बुरा है।)
foo()
ही bar()
बुलाया जाएगा। मैं कोड के कई "वास्तविक दुनिया" तत्वों को नहीं लाना चाहता था, जिससे आपको लगता है कि "आपको समस्या है एक्स से बचने के लिए लेकिन आपको समस्या है वाई" तरह की प्रतिक्रियाएं। लेकिन, अगर किसी को यह जानने की जरूरत है कि पृष्ठभूमि की मंजिला क्या है: set()
वास्तव में some_mutex_exit()
, check()
यह है try_enter_some_mutex()
, y
"कुछ वेटर हैं", foo()
"किसी को भी जागने के बिना बाहर निकलें", bar()
"वकप के लिए इंतजार कर रहा है" ... लेकिन, मैंने मना कर दिया इस डिज़ाइन पर यहाँ चर्चा करें - मैं इसे वास्तव में नहीं बदल सकता।
std::atomic_thread_fence(std::memory_order_seq_cst)
जिनके बारे में मुझे पता है, एक पूर्ण अवरोध के लिए संकलित करता है, लेकिन चूंकि पूरी अवधारणा एक कार्यान्वयन विवरण है जो आपको नहीं मिलेगा मानक में इसका कोई उल्लेख नहीं है। (सीपीयू स्मृति मॉडल आमतौर पर कर रहे हैं क्या reorerings अनुक्रमिक स्थिरता के सापेक्ष अनुमति दी जाती है के रूप में परिभाषित जैसे 86 है seq-सीएसटी + एक दुकान बफर डब्ल्यू / अग्रेषण।)