थ्रेड-सेफ्टी नियमों द्वारा सुझाए गए नॉन-कॉस्ट तर्क के साथ कॉपी कंस्ट्रक्टर?


9

मैं विरासत कोड के कुछ टुकड़े के लिए एक आवरण है।

class A{
   L* impl_; // the legacy object has to be in the heap, could be also unique_ptr
   A(A const&) = delete;
   L* duplicate(){L* ret; legacy_duplicate(impl_, &L); return ret;}
   ... // proper resource management here
};

इस विरासत कोड में, वह फ़ंक्शन जो "ऑब्जेक्ट को डुप्लिकेट करता है" थ्रेड सुरक्षित नहीं है (उसी तर्क पर कॉल करते समय), इसलिए यह constआवरण में चिह्नित नहीं है । मुझे लगता है कि आधुनिक नियमों का पालन करना: https://herbsutter.com/2013/01/01/video-you-dont-know-const-and-mutable/

यह duplicateएक कॉपी कंस्ट्रक्टर को लागू करने के लिए एक अच्छा तरीका है, सिवाय इसके कि यह नहीं है const। इसलिए मैं सीधे यह नहीं कर सकता:

class A{
   L* impl_; // the legacy object has to be in the heap
   A(A const& other) : L{other.duplicate()}{} // error calling a non-const function
   L* duplicate(){L* ret; legacy_duplicate(impl_, &ret); return ret;}
};

तो इस विरोधाभासी स्थिति से बाहर निकलने का रास्ता क्या है?

(मान लीजिए कि यह legacy_duplicateथ्रेड-सुरक्षित नहीं है, लेकिन मुझे पता है कि जब यह बाहर निकलता है, तो मूल स्थिति में वस्तु छोड़ देता है। C-फ़ंक्शन होने के कारण व्यवहार केवल प्रलेखित होता है, लेकिन कब्ज की कोई अवधारणा नहीं होती है।)

मैं कई संभावित परिदृश्यों के बारे में सोच सकता हूं:

(1) एक संभावना यह है कि सामान्य अर्थ के साथ कॉपी कंस्ट्रक्टर को लागू करने का कोई तरीका नहीं है। (हां, मैं ऑब्जेक्ट को स्थानांतरित कर सकता हूं और यह वह नहीं है जो मुझे चाहिए।)

(२) दूसरी ओर, किसी वस्तु की प्रतिलिपि बनाना इस अर्थ में गैर-थ्रेड-सेफ़ है कि सरल प्रकार की प्रतिलिपि बनाने से स्रोत को अर्ध-संशोधित अवस्था में पाया जा सकता है, इसलिए मैं बस आगे बढ़ सकता हूं और ऐसा कर सकता हूं,

class A{
   L* impl_;
   A(A const& other) : L{const_cast<A&>(other).duplicate()}{} // error calling a non-const function
   L* duplicate(){L* ret; legacy_duplicate(impl_, &ret); return ret;}
};

(3) या यहां तक ​​कि सिर्फ duplicateकॉन्स्टेबल की घोषणा करें और सभी संदर्भों में थ्रेड सुरक्षा के बारे में झूठ बोलें। (सभी लीगेसी फ़ंक्शन के बारे में परवाह नहीं है constइसलिए कंपाइलर भी शिकायत नहीं करेगा।)

class A{
   L* impl_;
   A(A const& other) : L{other.duplicate()}{}
   L* duplicate() const{L* ret; legacy_duplicate(impl_, &ret); return ret;}
};

(4) अंत में, मैं तर्क का अनुसरण कर सकता हूं और एक गैर-कांस्टेबल तर्क लेने वाला एक कॉपी-कंस्ट्रक्टर बना सकता हूं ।

class A{
   L* impl_;
   A(A const&) = delete;
   A(A& other) : L{other.duplicate()}{}
   L* duplicate(){L* ret; legacy_duplicate(impl_, &ret); return ret;}
};

यह पता चला है कि यह कई संदर्भों में काम करता है, क्योंकि ये ऑब्जेक्ट आमतौर पर नहीं होते हैं const

सवाल यह है कि क्या यह एक वैध या सामान्य मार्ग है?

मैं उनका नाम नहीं ले सकता, लेकिन मैं सहजता से उम्मीद करता हूं कि नॉन-कास्ट कॉपी कंस्ट्रक्टर होने की राह में बहुत सारी समस्याएं हैं। संभवतः इस सूक्ष्मता के कारण यह मूल्य-प्रकार के रूप में योग्य नहीं होगा।

(५) अंत में, हालांकि यह एक ओवरकिल लगता है और इसमें एक स्थिर रनटाइम लागत हो सकती है, मैं एक म्यूटेंट जोड़ सकता हूं:

class A{
   L* impl_;
   A(A const& other) : L{other.duplicate_locked()}{}
   L* duplicate(){
      L* ret; legacy_duplicate(impl_, &ret); return ret;
   }
   L* duplicate_locked() const{
      std::lock_guard<std::mutex> lk(mut);
      L* ret; legacy_duplicate(impl_, &ret); return ret;
   }
   mutable std::mutex mut;
};

लेकिन ऐसा करने के लिए मजबूर होना निराशावाद जैसा लगता है और वर्ग को बड़ा बनाता है। मुझे यकीन नहीं है। मैं वर्तमान में (4) , या (5) या दोनों के संयोजन की ओर झुक रहा हूं ।


संपादित करें 1:

एक अन्य विकल्प:

(6) डुप्लिकेट सदस्य फ़ंक्शन के सभी गैर-समझ के बारे में भूल जाओ और बस legacy_duplicateकंस्ट्रक्टर से कॉल करें और घोषणा करें कि कॉपी कंस्ट्रक्टर थ्रेड सुरक्षित नहीं है। (और यदि आवश्यक हो तो प्रकार का एक और धागा-सुरक्षित वर्जन बनाएं A_mt)

class A{
   L* impl_;
   A(A const& other){legacy_duplicate(other.impl_, &impl_);}
};

संपादित करें 2:

यह एक अच्छा मॉडल हो सकता है कि विरासत समारोह क्या करता है। ध्यान दें कि इनपुट को स्पर्श करके कॉल पहले तर्क द्वारा दर्शाए गए मूल्य के संबंध में सुरक्षित नहीं है।

void legacy_duplicate(L* in, L** out){
   *out = new L{};
   char tmp = in[0];
   in[0] = tmp; 
   std::memcpy(*out, in, sizeof *in); return; 
}

EDIT 3: मैंने हाल ही में सीखा कि std::auto_ptrएक गैर-कॉन्स्टेबल "कॉपी" कंस्ट्रक्टर होने की एक समान समस्या थी। इसका प्रभाव यह था कि auto_ptrएक कंटेनर के अंदर इस्तेमाल नहीं किया जा सकता था । https://www.quantstart.com/articles/STL-Containers-and-Auto_ptrs-Why-They-Dont-Mix/


1
" इस विरासत कोड में वह फ़ंक्शन जो किसी ऑब्जेक्ट को डुप्लिकेट करता है, थ्रेड सुरक्षित नहीं है (उसी तर्क पर कॉल करते समय) " क्या आप इसके बारे में निश्चित हैं? क्या कुछ राज्य निहित नहीं है Lजो एक नया Lउदाहरण बनाकर संशोधित किया गया है ? यदि नहीं, तो आप क्यों मानते हैं कि यह ऑपरेशन थ्रेड-सुरक्षित नहीं है?
निकोल बोलस

हां, यही स्थिति है। ऐसा लगता है कि एक्सनेशन के दौरान पहले तर्क की आंतरिक स्थिति को संशोधित किया गया है। किसी कारण से (कुछ "अनुकूलन" या खराब डिज़ाइन या बस विनिर्देश द्वारा) फ़ंक्शन legacy_duplicateको दो अलग-अलग थ्रेड्स से एक ही पहले तर्क के साथ नहीं कहा जा सकता है।
alfC

@TedLyngmo ठीक है मैंने किया। यद्यपि तकनीकी रूप से c ++ प्री 11 कॉन्स्ट में थ्रेड्स की उपस्थिति में अधिक फजी अर्थ होता है।
alfC

@TedLyngmo हाँ, यह एक बहुत अच्छा वीडियो है। यह एक अफ़सोस की बात है कि वीडियो सिर्फ उचित सदस्यों के साथ काम करता है और निर्माण की समस्या को नहीं छूता है ("अन्य" ऑब्जेक्ट पर भी कसना था)। परिप्रेक्ष्य में इस आवरण धागे को अमूर्त (और एक ठोस म्यूटेक्स) की एक और परत को जोड़ने के बिना कॉपी करने पर कोई आंतरिक तरीका सुरक्षित नहीं हो सकता है।
alfC

हाँ, ठीक है, मुझे उलझन में डाल दिया और मैं शायद उन लोगों में से एक हूं जो नहीं जानते कि constवास्तव में इसका क्या मतलब है। :-) मुझे लगता है कि const&जब तक मैं संशोधित नहीं करता, तब तक मैं अपनी कॉपी ctor में लेने के बारे में दो बार नहीं सोचूंगा other। मैं हमेशा थ्रेड सेफ्टी के बारे में सोचता हूं, क्योंकि जो कुछ भी कई थ्रेड्स से एक्सेस करने की जरूरत होती है, उसके ऊपर एनकैप्सुलेशन के माध्यम से जुड़ता है, और मैं वास्तव में उत्तर की प्रतीक्षा कर रहा हूं।
टेड लिंगमो

जवाबों:


0

मैं आपके दोनों विकल्पों (4) और (5) को शामिल करूंगा, लेकिन प्रदर्शन के लिए आवश्यक होने पर स्पष्ट रूप से थ्रेड-असुरक्षित व्यवहार का विकल्प चुनता हूं।

यहाँ एक पूर्ण उदाहरण है।

#include <cstdlib>
#include <thread>

struct L {
  int val;
};

void legacy_duplicate(const L* in, L** out) {
  *out = new L{};
  std::memcpy(*out, in, sizeof *in);
  return;
}

class A {
 public:
  A(L* l) : impl_{l} {}
  A(A const& other) : impl_{other.duplicate_locked()} {}

  A copy_unsafe_for_multithreading() { return {duplicate()}; }

  L* impl_;

  L* duplicate() {
    printf("in duplicate\n");
    L* ret;
    legacy_duplicate(impl_, &ret);
    return ret;
  }
  L* duplicate_locked() const {
    std::lock_guard<std::mutex> lk(mut);
    printf("in duplicate_locked\n");
    L* ret;
    legacy_duplicate(impl_, &ret);
    return ret;
  }
  mutable std::mutex mut;
};

int main() {
  A a(new L{1});
  const A b(new L{2});

  A c = a;
  A d = b;

  A e = a.copy_unsafe_for_multithreading();
  A f = const_cast<A&>(b).copy_unsafe_for_multithreading();

  printf("\npointers:\na=%p\nb=%p\nc=%p\nc=%p\nd=%p\nf=%p\n\n", a.impl_,
     b.impl_, c.impl_, d.impl_, e.impl_, f.impl_);

  printf("vals:\na=%d\nb=%d\nc=%d\nc=%d\nd=%d\nf=%d\n", a.impl_->val,
     b.impl_->val, c.impl_->val, d.impl_->val, e.impl_->val, f.impl_->val);
}

आउटपुट:

in duplicate_locked
in duplicate_locked
in duplicate
in duplicate

pointers:
a=0x7f85e8c01840
b=0x7f85e8c01850
c=0x7f85e8c01860
c=0x7f85e8c01870
d=0x7f85e8c01880
f=0x7f85e8c01890

vals:
a=1
b=2
c=1
c=2
d=1
f=2

यह Google शैली मार्गदर्शिका का अनुसरण करता है जिसमें constथ्रेड सुरक्षा का संचार होता है, लेकिन आपके API को कॉल करने वाला कोड उपयोग करके ऑप्ट-आउट कर सकता हैconst_cast


उत्तर के लिए धन्यवाद, मुझे लगता है कि यह आपके asnwer को नहीं बदलता है और मुझे यकीन नहीं है लेकिन इसके लिए एक बेहतर मॉडल legacy_duplicateहो सकता है void legacy_duplicate(L* in, L** out) { *out = new L{}; char tmp = in[0]; /*some weird call here*/; in[0] = tmp; std::memcpy(*out, in, sizeof *in); return; }(यानी गैर- in
कास्ट

आपका उत्तर बहुत दिलचस्प है क्योंकि इसे विकल्प (4) और विकल्प के स्पष्ट संस्करण (2) के साथ जोड़ा जा सकता है। यही है, A a2(a1)थ्रेड को सुरक्षित (या हटाए जाने) की A a2(const_cast<A&>(a1))कोशिश कर सकता है और थ्रेड को सुरक्षित रखने की कोशिश नहीं करेगा।
alfC

2
हां, यदि आप Aथ्रेड-सेफ और थ्रेड-असुरक्षित दोनों संदर्भों में उपयोग करने की योजना बनाते हैं, तो आपको const_castकॉलिंग कोड को खींचना चाहिए ताकि यह स्पष्ट हो जाए कि थ्रेड-सेफ्टी का उल्लंघन करने के लिए जाना जाता है। एपीआई (म्यूटेक्स) के पीछे अतिरिक्त सुरक्षा को धकेलना ठीक है, लेकिन अनिश्चितता (कॉन्स्टेकास्ट) को छिपाने के लिए ठीक नहीं है।
माइकल ग्राज़

0

TLDR: या तो अपने दोहराव समारोह के कार्यान्वयन ठीक है, या एक म्युटेक्स परिचय (या कुछ और अधिक उपयुक्त लॉकिंग डिवाइस, शायद एक spinlock, या भारी कुछ भी करने से पहले सुनिश्चित करें कि आपके म्युटेक्स स्पिन करने के लिए कॉन्फ़िगर किया गया है) अब के लिए , तो दोहराव के कार्यान्वयन ठीक और लॉकिंग को हटा दें जब लॉकिंग वास्तव में एक समस्या बन जाती है।

मुझे लगता है कि नोटिस करने के लिए एक महत्वपूर्ण बिंदु यह है कि आप एक ऐसी सुविधा जोड़ रहे हैं जो पहले मौजूद नहीं थी: एक ही समय में कई थ्रेड्स से किसी ऑब्जेक्ट को डुप्लिकेट करने की क्षमता।

जाहिर है, जिन शर्तों के तहत आपने रिश्वत दी है, वह बग - दौड़ की स्थिति रही होगी, यदि आप पहले भी ऐसा करते रहे हैं, तो किसी प्रकार के बाहरी तुल्यकालन का उपयोग किए बिना।

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

इसके अलावा, वस्तु की कथित जटिलता के आधार पर - विशेष उपचार द्वारा आप इसे दे रहे हैं, मैं यह मानने जा रहा हूं कि दोहराव प्रक्रिया एक तुच्छ नहीं है, इसलिए, प्रदर्शन के मामले में पहले से ही काफी महंगा है।

उपरोक्त के आधार पर, आपके पास दो रास्ते हैं जिनका आप अनुसरण कर सकते हैं:

ए) आप जानते हैं कि कई थ्रेड्स से इस ऑब्जेक्ट को कॉपी करना अक्सर अतिरिक्त लॉकिंग के ओवरहेड के लिए पर्याप्त नहीं होगा - महंगा तुच्छ सस्ते, कम से कम यह देखते हुए कि मौजूदा दोहराव प्रक्रिया अपने आप ही काफी महंगा है, यदि आप एक का उपयोग करते हैं स्पिनलॉक / प्री-स्पिनिंग म्यूटेक्स, और इस पर कोई विवाद नहीं है।

बी) आपको संदेह है कि कई थ्रेड्स से कॉपी करने से एक्स्ट्रा लॉकिंग की समस्या हो सकती है। फिर आपके पास वास्तव में केवल एक विकल्प है - अपना डुप्लिकेट कोड ठीक करें। यदि आप इसे ठीक नहीं करते हैं, तो आपको वैसे भी ताला लगाने की आवश्यकता होगी, चाहे वह इस अमूर्त की परत पर हो या कहीं और, लेकिन आपको बग की आवश्यकता नहीं होने पर इसकी आवश्यकता होगी - और जैसा कि हमने स्थापित किया है, इस रास्ते में, आप मान लेते हैं वह लॉकिंग बहुत महंगा हो जाएगा, इसलिए, एकमात्र विकल्प डुप्लिकेट कोड को ठीक कर रहा है।

मुझे संदेह है कि आप वास्तव में ए स्थिति में हैं, और बस एक स्पिनलॉक / स्पिनिंग म्यूटेक्स जोड़ रहे हैं जो निर्विरोध होने पर कोई प्रदर्शन जुर्माना नहीं है, बस ठीक काम करेगा (इसे बेंचमार्क करना याद रखें, हालांकि)।

सिद्धांत रूप में, एक और स्थिति है:

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

इस मामले में, मैं निम्नलिखित सुझाव देता हूं: किसी को भी गलती से उपयोग करने से रोकने के लिए, डिफ़ॉल्ट कॉपी निर्माणकर्ताओं / ऑपरेटरों को हटा दिया गया है। दो स्पष्ट रूप से कॉल करने योग्य दोहराव के तरीके बनाएं, एक धागा सुरक्षित एक, और एक धागा असुरक्षित; संदर्भ के आधार पर, अपने उपयोगकर्ताओं को स्पष्ट रूप से कॉल करें। फिर से, स्वीकार्य एकल थ्रेड प्रदर्शन और सुरक्षित मल्टी थ्रेडिंग को प्राप्त करने का कोई अन्य तरीका नहीं है, यदि आप वास्तव में इस स्थिति में हैं और आप मौजूदा दोहराव कार्यान्वयन को ठीक नहीं कर सकते हैं । लेकिन मुझे लगता है कि यह बहुत कम संभावना है कि आप वास्तव में हैं।

बस उस म्यूटेक्स / स्पिनलॉक और बेंचमार्क को जोड़ें।


क्या आप मुझे C ++ में स्पिनलॉक / प्री-स्पिनिंग म्यूटेक्स पर सामग्री के लिए इंगित कर सकते हैं? क्या यह कुछ अधिक जटिल है कि क्या प्रदान किया जाता है std::mutex? डुप्लिकेट फ़ंक्शन कोई रहस्य नहीं है, मैंने उच्च स्तर पर समस्या को रखने और एमपीआई के बारे में उत्तर प्राप्त नहीं करने के लिए इसका उल्लेख नहीं किया। लेकिन जब से तुम गए कि मैं तुम्हें और अधिक जानकारी दे सकता हूं। विरासत समारोह है MPI_Comm_dupऔर प्रभावी गैर-थ्रेड सफेदी यहाँ वर्णित है (मैंने इसकी पुष्टि की है) github.com/pmodels/mpich/issues/3234 । यही कारण है कि मैं डुप्लिकेट को ठीक नहीं कर सकता। (इसके अलावा, अगर मैं एक म्यूटेक्स जोड़ता हूं तो मुझे सभी एमपीआई कॉल को थ्रेड-सेफ बनाने के लिए लुभाया जाएगा।)
एल्फ सी सी

अफसोस की बात है कि मैं ज्यादा std :: mutex नहीं जानता, लेकिन मुझे लगता है कि यह प्रक्रिया को सोने से पहले कुछ कताई करता है। एक जानी-पहचानी सिंक्रोनाइज़ेशन डिवाइस, जहाँ आपको इसे मैन्युअल रूप से नियंत्रित करना है: docs.microsoft.com/en-us/windows/win32/api/synchapi/… मैंने प्रदर्शन की तुलना नहीं की है, लेकिन ऐसा लगता है कि std :: mutex अब बेहतर: stackoverflow.com/questions/9997473/… और का उपयोग करके कार्यान्वित किया गया: docs.microsoft.com/en-us/windows/win32/sync/…
DeducibleSteak

ऐसा लगता है कि इस पर ध्यान देने के लिए सामान्य विचारों का अच्छा विवरण है: stackoverflow.com/questions/5869825/…
DeducibleSteak

धन्यवाद फिर से, मैं लिनक्स में हूँ अगर वह मायने रखता है।
alfC

यहां कुछ हद तक विस्तृत प्रदर्शन की तुलना है (एक अलग भाषा के लिए, लेकिन मुझे लगता है कि यह सूचनात्मक और संकेत है कि क्या उम्मीद की जाए): matklad.github.io/2020/01/04/… TLDR है - स्पिनलॉक एक बेहद छोटी जीत मार्जिन जब कोई विवाद नहीं है, तो विवाद होने पर बुरी तरह हार सकता है।
डेड्यूसीबल स्टिक
हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.