डिजाइन के अनुसार, std::mutex
चल नहीं है और न ही प्रतिलिपि बनाई जा सकती है। इसका अर्थ है कि A
म्यूटेक्स रखने वाले वर्ग को डिफ़ॉल्ट मूव कंस्ट्रक्टर प्राप्त नहीं होगा।
मैं इस प्रकार A
को एक थ्रेड-सुरक्षित तरीके से कैसे चल सकता हूं ?
डिजाइन के अनुसार, std::mutex
चल नहीं है और न ही प्रतिलिपि बनाई जा सकती है। इसका अर्थ है कि A
म्यूटेक्स रखने वाले वर्ग को डिफ़ॉल्ट मूव कंस्ट्रक्टर प्राप्त नहीं होगा।
मैं इस प्रकार A
को एक थ्रेड-सुरक्षित तरीके से कैसे चल सकता हूं ?
std::lock_guard
विधि को स्कोप किया जाता है।
जवाबों:
चलो कोड के एक बिट के साथ शुरू करते हैं:
class A
{
using MutexType = std::mutex;
using ReadLock = std::unique_lock<MutexType>;
using WriteLock = std::unique_lock<MutexType>;
mutable MutexType mut_;
std::string field1_;
std::string field2_;
public:
...
मैंने कुछ विचारोत्तेजक प्रकार के उपनामों को वहाँ रखा है कि हम वास्तव में C ++ 11 में लाभ नहीं लेंगे, लेकिन C ++ 14 में बहुत अधिक उपयोगी बन जाते हैं। धीरज रखो, हम वहाँ पहुँचेंगे।
आपका प्रश्न निम्नलिखित पर उबलता है:
मैं इस वर्ग के लिए मूव कंस्ट्रक्टर और मूव असाइनमेंट ऑपरेटर कैसे लिखूँ?
हम कदम निर्माता के साथ शुरू करेंगे।
कंस्ट्रक्टर को मूव करें
ध्यान दें कि सदस्य mutex
बनाया गया है mutable
। सख्ती से बोलना इस कदम के सदस्यों के लिए आवश्यक नहीं है, लेकिन मुझे लगता है कि आप भी कॉपी सदस्यों को चाहते हैं। यदि ऐसा नहीं है, तो म्यूटेक्स बनाने की कोई आवश्यकता नहीं है mutable
।
निर्माण करते समय A
, आपको लॉक करने की आवश्यकता नहीं है this->mut_
। लेकिन आपको mut_
उस ऑब्जेक्ट को लॉक करने की आवश्यकता है जिसे आप (स्थानांतरित या प्रतिलिपि) से निर्माण कर रहे हैं। यह इस तरह किया जा सकता है:
A(A&& a)
{
WriteLock rhs_lk(a.mut_);
field1_ = std::move(a.field1_);
field2_ = std::move(a.field2_);
}
ध्यान दें कि हमें पहले के सदस्यों का निर्माण डिफ़ॉल्ट रूप से करना था this
, और फिर a.mut_
लॉक होने के बाद ही उन्हें मान असाइन करें ।
असाइनमेंट ले जाएं
चाल असाइनमेंट ऑपरेटर काफी अधिक जटिल है क्योंकि आपको नहीं पता है कि कुछ अन्य थ्रेड असाइनमेंट अभिव्यक्ति के lhs या rs तक पहुँच रहे हैं या नहीं। और सामान्य तौर पर, आपको निम्नलिखित परिदृश्य की रक्षा करने की आवश्यकता है:
// Thread 1
x = std::move(y);
// Thread 2
y = std::move(x);
यहाँ चाल असाइनमेंट ऑपरेटर है जो उपरोक्त परिदृश्य को सही ढंग से रखता है:
A& operator=(A&& a)
{
if (this != &a)
{
WriteLock lhs_lk(mut_, std::defer_lock);
WriteLock rhs_lk(a.mut_, std::defer_lock);
std::lock(lhs_lk, rhs_lk);
field1_ = std::move(a.field1_);
field2_ = std::move(a.field2_);
}
return *this;
}
ध्यान दें कि एक के बाद एक std::lock(m1, m2)
को लॉक करने के बजाय, दो म्यूटेक्स को लॉक करने के लिए उपयोग करना चाहिए । यदि आप उन्हें एक के बाद एक लॉक करते हैं, तो जब दो धागे विपरीत क्रम में दो वस्तुओं को असाइन करते हैं जैसा कि ऊपर दिखाया गया है, तो आप एक गतिरोध प्राप्त कर सकते हैं। की बात std::lock
है कि गतिरोध से बचना है।
कापी कंस्ट्रक्टर
आपने कॉपी सदस्यों के बारे में नहीं पूछा, लेकिन हम अब उनके बारे में बात कर सकते हैं (यदि आप नहीं, तो किसी को उनकी आवश्यकता होगी)।
A(const A& a)
{
ReadLock rhs_lk(a.mut_);
field1_ = a.field1_;
field2_ = a.field2_;
}
कॉपी कंस्ट्रक्टर काफी हद तक मूव कंस्ट्रक्टर की तरह दिखता है, सिवाय इसके कि ReadLock
उर्फ का उपयोग किया जाता है WriteLock
। वर्तमान में इन दोनों उपनामों std::unique_lock<std::mutex>
और इसलिए यह वास्तव में कोई फर्क नहीं पड़ता है।
लेकिन C ++ 14 में, आपके पास यह कहने का विकल्प होगा:
using MutexType = std::shared_timed_mutex;
using ReadLock = std::shared_lock<MutexType>;
using WriteLock = std::unique_lock<MutexType>;
यह एक अनुकूलन हो सकता है, लेकिन निश्चित रूप से नहीं। आपको यह निर्धारित करने के लिए मापना होगा कि क्या यह है। लेकिन इस बदलाव के साथ, एक निर्माण कॉपी कर सकते हैं से अधिक थ्रेड एक साथ में एक ही आरएचएस। C ++ 11 समाधान आपको ऐसे थ्रेड्स को अनुक्रमिक बनाने के लिए बाध्य करता है, भले ही rhs को संशोधित नहीं किया जा रहा हो।
असाइनमेंट कॉपी करें
पूर्णता के लिए, यहां कॉपी असाइनमेंट ऑपरेटर है, जिसे बाकी चीजों के बारे में पढ़ने के बाद काफी आत्म व्याख्यात्मक होना चाहिए:
A& operator=(const A& a)
{
if (this != &a)
{
WriteLock lhs_lk(mut_, std::defer_lock);
ReadLock rhs_lk(a.mut_, std::defer_lock);
std::lock(lhs_lk, rhs_lk);
field1_ = a.field1_;
field2_ = a.field2_;
}
return *this;
}
और आदि।
A
यदि आप एक बार में कई थ्रेड्स को कॉल करने में सक्षम होने की अपेक्षा करते हैं, तो किसी भी अन्य सदस्य या नि: शुल्क फ़ंक्शन को भी एक्सेस करना होगा। उदाहरण के लिए, यहाँ है swap
:
friend void swap(A& x, A& y)
{
if (&x != &y)
{
WriteLock lhs_lk(x.mut_, std::defer_lock);
WriteLock rhs_lk(y.mut_, std::defer_lock);
std::lock(lhs_lk, rhs_lk);
using std::swap;
swap(x.field1_, y.field1_);
swap(x.field2_, y.field2_);
}
}
ध्यान दें कि यदि आप सिर्फ std::swap
काम करने पर निर्भर हैं , तो लॉकिंग गलत ग्रैन्युलैरिटी पर std::swap
होगा, आंतरिक रूप से प्रदर्शन करने वाले तीन चालों के बीच लॉकिंग और अनलॉकिंग होगी।
दरअसल, इस बारे में सोच swap
आपको एपीआई में "थ्रेड-सेफ" प्रदान करने की आवश्यकता हो सकती है A
, जो सामान्य रूप से "लॉकिंग ग्रैन्युलैरिटी" समस्या के कारण "नॉन-थ्रेड-सेफ" एपीआई से अलग होगी।
"स्व-स्वैप" से बचाने की आवश्यकता पर भी ध्यान दें। "स्व-स्वैप" एक नो-ऑप होना चाहिए। स्व-जांच के बिना एक ही म्यूटेक्स को पुन: लॉक किया जाएगा। यह भी का उपयोग करके स्वयं की जांच के बिना हल किया जा सकता std::recursive_mutex
के लिए MutexType
।
अपडेट करें
नीचे दिए गए टिप्पणियों में याक को कॉपी और मूव कन्स्ट्रक्टर्स (और उसके पास एक बिंदु) में चीजों को डिफ़ॉल्ट रूप से बनाने के बारे में बहुत दुखी है। क्या आपको इस मुद्दे के बारे में पर्याप्त रूप से महसूस करना चाहिए, इतना है कि आप इस पर स्मृति खर्च करने के लिए तैयार हैं, आप इसे इस तरह से बचा सकते हैं:
डेटा सदस्यों के रूप में आपको जो भी लॉक प्रकार की आवश्यकता है, उसे जोड़ें। इन सदस्यों को संरक्षित किए जाने वाले डेटा से पहले आना चाहिए:
mutable MutexType mut_;
ReadLock read_lock_;
WriteLock write_lock_;
// ... other data members ...
और फिर कंस्ट्रक्टर्स में (जैसे कॉपी कंस्ट्रक्टर) ऐसा करें:
A(const A& a)
: read_lock_(a.mut_)
, field1_(a.field1_)
, field2_(a.field2_)
{
read_lock_.unlock();
}
इससे पहले कि मुझे इस अपडेट को पूरा करने का मौका मिलता, उफ़, यक्क ने अपनी टिप्पणी मिटा दी। लेकिन वह इस मुद्दे को आगे बढ़ाने और इस जवाब में समाधान पाने के लिए श्रेय के हकदार हैं।
अपडेट २
और इस अच्छे सुझाव के साथ dyp आया:
A(const A& a)
: A(a, ReadLock(a.mut_))
{}
private:
A(const A& a, ReadLock rhs_lk)
: field1_(a.field1_)
, field2_(a.field2_)
{}
mutexes
क्लास के प्रकारों में डालना "एक सही तरीका" नहीं है। यह टूलबॉक्स में एक उपकरण है और यदि आप इसका उपयोग करना चाहते हैं, तो यह है।
यह देखते हुए कि यह जवाब देने के लिए एक अच्छा, साफ, आसान तरीका नहीं है - मुझे लगता है कि एंटोन का समाधान सही है, लेकिन निश्चित रूप से बहस का मुद्दा है, जब तक कि बेहतर जवाब नहीं आता है, मैं इस तरह के वर्ग को ढेर पर रखने और इसकी देखभाल करने की सलाह दूंगा। एक के माध्यम से std::unique_ptr
:
auto a = std::make_unique<A>();
यह अब पूरी तरह से चल प्रकार का है और कोई भी व्यक्ति जिसके पास आंतरिक म्यूटेक्स पर एक ताला है, जबकि एक चाल होती है वह अभी भी सुरक्षित है, भले ही इसकी बहस करने योग्य है कि क्या यह करना एक अच्छी बात है
यदि आपको कॉपी शब्दार्थों का उपयोग करने की आवश्यकता है
auto a2 = std::make_shared<A>();
यह उलटा जवाब है। टाइप के आधार के रूप में "इस ऑब्जेक्ट को सिंक्रनाइज़ करने की आवश्यकता है" को एम्बेड करने के बजाय, इसे किसी भी प्रकार के तहत इंजेक्ट करें।
आप एक सिंक्रनाइज़ ऑब्जेक्ट के साथ बहुत अलग तरीके से व्यवहार करते हैं। एक बड़ा मुद्दा यह है कि आपको गतिरोधों (कई वस्तुओं को लॉक करना) के बारे में चिंता करना होगा। यह मूल रूप से आपका "ऑब्जेक्ट का डिफ़ॉल्ट संस्करण" कभी नहीं होना चाहिए: सिंक्रनाइज़ किए गए ऑब्जेक्ट उन ऑब्जेक्ट्स के लिए हैं जो विवाद में होंगे, और आपका लक्ष्य थ्रेड्स के बीच विवाद को कम करना चाहिए, न कि इसे गलीचा के नीचे स्वीप करें।
लेकिन वस्तुओं को सिंक्रनाइज़ करना अभी भी उपयोगी है। एक सिंक्रनाइज़र से विरासत में मिलने के बजाय, हम एक वर्ग लिख सकते हैं जो सिंक्रोनाइज़ेशन में एक मनमाना प्रकार लपेटता है। उपयोगकर्ताओं को ऑब्जेक्ट पर संचालन करने के लिए कुछ हुप्स के माध्यम से कूदना पड़ता है अब यह सिंक्रनाइज़ है, लेकिन वे ऑब्जेक्ट पर कुछ हाथ से संचालित सीमित सेट तक सीमित नहीं हैं। वे ऑब्जेक्ट पर एक में कई ऑपरेशन की रचना कर सकते हैं, या कई ऑब्जेक्ट पर एक ऑपरेशन कर सकते हैं।
यहाँ एक मनमाना प्रकार के आसपास एक सिंक्रनाइज़ रैपर है T
:
template<class T>
struct synchronized {
template<class F>
auto read(F&& f) const&->std::result_of_t<F(T const&)> {
return access(std::forward<F>(f), *this);
}
template<class F>
auto read(F&& f) &&->std::result_of_t<F(T&&)> {
return access(std::forward<F>(f), std::move(*this));
}
template<class F>
auto write(F&& f)->std::result_of_t<F(T&)> {
return access(std::forward<F>(f), *this);
}
// uses `const` ness of Syncs to determine access:
template<class F, class... Syncs>
friend auto access( F&& f, Syncs&&... syncs )->
std::result_of_t< F(decltype(std::forward<Syncs>(syncs).t)...) >
{
return access2( std::index_sequence_for<Syncs...>{}, std::forward<F>(f), std::forward<Syncs>(syncs)... );
};
synchronized(synchronized const& o):t(o.read([](T const&o){return o;})){}
synchronized(synchronized && o):t(std::move(o).read([](T&&o){return std::move(o);})){}
// special member functions:
synchronized( T & o ):t(o) {}
synchronized( T const& o ):t(o) {}
synchronized( T && o ):t(std::move(o)) {}
synchronized( T const&& o ):t(std::move(o)) {}
synchronized& operator=(T const& o) {
write([&](T& t){
t=o;
});
return *this;
}
synchronized& operator=(T && o) {
write([&](T& t){
t=std::move(o);
});
return *this;
}
private:
template<class X, class S>
static auto smart_lock(S const& s) {
return std::shared_lock< std::shared_timed_mutex >(s.m, X{});
}
template<class X, class S>
static auto smart_lock(S& s) {
return std::unique_lock< std::shared_timed_mutex >(s.m, X{});
}
template<class L>
static void lock(L& lockable) {
lockable.lock();
}
template<class...Ls>
static void lock(Ls&... lockable) {
std::lock( lockable... );
}
template<size_t...Is, class F, class...Syncs>
friend auto access2( std::index_sequence<Is...>, F&&f, Syncs&&...syncs)->
std::result_of_t< F(decltype(std::forward<Syncs>(syncs).t)...) >
{
auto locks = std::make_tuple( smart_lock<std::defer_lock_t>(syncs)... );
lock( std::get<Is>(locks)... );
return std::forward<F>(f)(std::forward<Syncs>(syncs).t ...);
}
mutable std::shared_timed_mutex m;
T t;
};
template<class T>
synchronized< T > sync( T&& t ) {
return {std::forward<T>(t)};
}
C ++ 14 और C ++ 1z विशेषताएं शामिल हैं।
यह मानता है कि const
ऑपरेशन कई-पाठक सुरक्षित हैं (जो std
कंटेनर को मानते हैं)।
उपयोग इस तरह दिखता है:
synchronized<int> x = 7;
x.read([&](auto&& v){
std::cout << v << '\n';
});
एक के लिए int
सिंक्रनाइज़ का उपयोग के साथ।
मैं होने के खिलाफ सलाह देंगे synchronized(synchronized const&)
। इसकी जरूरत शायद ही हो।
आप की जरूरत है synchronized(synchronized const&)
, मैं बदलने के लिए परीक्षा होगी T t;
साथ std::aligned_storage
, मैनुअल प्लेसमेंट निर्माण की इजाजत दी, और मैनुअल विनाश करते हैं। यह उचित जीवनकाल प्रबंधन की अनुमति देता है।
इसे छोड़कर, हम स्रोत की प्रतिलिपि बना सकते हैं T
, फिर उससे पढ़ सकते हैं:
synchronized(synchronized const& o):
t(o.read(
[](T const&o){return o;})
)
{}
synchronized(synchronized && o):
t(std::move(o).read(
[](T&&o){return std::move(o);})
)
{}
असाइनमेंट के लिए:
synchronized& operator=(synchronized const& o) {
access([](T& lhs, T const& rhs){
lhs = rhs;
}, *this, o);
return *this;
}
synchronized& operator=(synchronized && o) {
access([](T& lhs, T&& rhs){
lhs = std::move(rhs);
}, *this, std::move(o));
return *this;
}
friend void swap(synchronized& lhs, synchronized& rhs) {
access([](T& lhs, T& rhs){
using std::swap;
swap(lhs, rhs);
}, *this, o);
}
प्लेसमेंट और संरेखित स्टोरेज संस्करण थोड़ा गड़बड़ है। अधिकांश पहुंच को t
सदस्य फ़ंक्शन द्वारा प्रतिस्थापित किया जाएगा T&t()
और T const&t()const
निर्माण को छोड़कर, जहां आपको कुछ हुप्स के माध्यम से कूदना होगा।
synchronized
कक्षा के हिस्से के बजाय एक आवरण बनाकर , हम सभी को यह सुनिश्चित करना होगा कि वर्ग आंतरिक const
रूप से बहु-पाठक होने के नाते सम्मान करता है , और इसे एकल-सूत्र में लिखता है।
में दुर्लभ मामलों में हम एक तुल्यकालन उदाहरण की जरूरत है, हम ऊपर की तरह हुप्स के माध्यम से कूद।
उपरोक्त में किसी भी टाइपोस के लिए माफी। शायद कुछ हैं।
ऊपर एक साइड का लाभ यह है कि synchronized
वस्तुओं पर (समान प्रकार के) एन-एरी मनमाना संचालन एक साथ काम करते हैं, बिना हाथ से इसे हार्ड-कोड किए बिना। एक मित्र घोषणा में जोड़ें और synchronized
कई प्रकार की n-ary ऑब्जेक्ट एक साथ काम कर सकते हैं। मुझे access
उस मामले में अधिभार संघर्ष से निपटने के लिए एक इनलाइन मित्र होने से बाहर होना पड़ सकता है ।
म्यूटेक्स और सी ++ मूवमेंट शब्द का उपयोग थ्रेड्स के बीच डेटा को सुरक्षित और कुशलतापूर्वक स्थानांतरित करने का एक शानदार तरीका है।
एक 'निर्माता' थ्रेड की कल्पना करें जो स्ट्रिंग्स के बैच बनाता है और उन्हें (एक या अधिक) उपभोक्ताओं को प्रदान करता है। उन बैचों को एक ऑब्जेक्ट (संभावित बड़ी) std::vector<std::string>
वस्तुओं द्वारा दर्शाया जा सकता है । हम पूरी तरह से उन वैक्टरों की आंतरिक स्थिति को अनावश्यक रूप से दोहराए बिना अपने उपभोक्ताओं में ले जाना चाहते हैं।
आप बस वस्तु के भाग के रूप में म्यूटेक्स को पहचानते हैं जो वस्तु की स्थिति का हिस्सा नहीं है। यही है, आप म्यूटेक्स को स्थानांतरित नहीं करना चाहते हैं।
आपको किस लॉकिंग की आवश्यकता है यह आपके एल्गोरिथ्म पर निर्भर करता है या आपकी वस्तुओं का सामान्यीकरण कैसे होता है और आप किस सीमा तक उपयोग करते हैं।
यदि आप केवल एक साझा राज्य 'निर्माता' ऑब्जेक्ट से थ्रेड-लोकल 'उपभोग' ऑब्जेक्ट पर ले जाते हैं, तो आप केवल ऑब्जेक्ट के लिए ले जाया गया लॉक करना ठीक हो सकता है ।
यदि यह अधिक सामान्य डिज़ाइन है तो आपको दोनों को लॉक करने की आवश्यकता होगी। ऐसे मामले में आपको मृत-लॉकिंग पर विचार करने की आवश्यकता है।
यदि यह एक संभावित मुद्दा है, तो std::lock()
एक गतिरोध मुक्त तरीके से दोनों म्यूटेक्स पर ताले प्राप्त करने के लिए उपयोग करें।
http://en.cppreference.com/w/cpp/thread/lock
अंतिम नोट के रूप में आपको यह सुनिश्चित करने की आवश्यकता है कि आप चाल शब्दार्थ को समझें। स्मरण करो कि वस्तु से स्थानांतरित वस्तु को वैध लेकिन अज्ञात अवस्था में छोड़ दिया जाता है। यह पूरी तरह से संभव है कि चाल का प्रदर्शन न करने वाले धागे के पास उस वस्तु से ले जाने का प्रयास करने का एक वैध कारण हो जब वह उस वैध लेकिन अज्ञात स्थिति को पा सकता है।
फिर से मेरा प्रोड्यूसर स्ट्रिंग्स को पीट रहा है और उपभोक्ता पूरा लोड निकाल रहा है। उस स्थिति में हर बार जब निर्माता वेक्टर को जोड़ने की कोशिश करता है तो उसे वेक्टर गैर-रिक्त या खाली मिल सकता है।
संक्षेप में अगर संभावित समवर्ती का उपयोग वस्तु राशियों से किसी लेखन में स्थानांतरित हो जाए तो यह ठीक है। यदि यह एक पढ़ने के लिए है, तो इसके बारे में सोचें कि मनमाना राज्य पढ़ने के लिए क्यों ठीक है।
सबसे पहले, आपके डिज़ाइन में कुछ गड़बड़ होनी चाहिए यदि आप किसी म्यूटेक्स युक्त ऑब्जेक्ट को स्थानांतरित करना चाहते हैं।
लेकिन अगर आप इसे वैसे भी करने का फैसला करते हैं, तो आपको मूव कंस्ट्रक्टर में एक नया म्यूटेक्स बनाना होगा, जैसे:
// movable
struct B{};
class A {
B b;
std::mutex m;
public:
A(A&& a)
: b(std::move(a.b))
// m is default-initialized.
{
}
};
यह थ्रेड-सुरक्षित है, क्योंकि मूव कंस्ट्रक्टर सुरक्षित रूप से मान सकता है कि इसके तर्क का उपयोग कहीं और नहीं किया गया है, इसलिए लॉजिक को लॉक करने की आवश्यकता नहीं है।
A a; A a2(std::move(a)); do some stuff with a
।
new
से उदाहरण की सिफारिश करूंगा और इसे ऐसे में रखूंगा std::unique_ptr
- जो क्लीनर लगता है और भ्रम की स्थिति पैदा करने की संभावना नहीं है। अच्छा प्रश्न।