ตามการออกแบบstd::mutex
ไม่สามารถเคลื่อนย้ายหรือคัดลอกได้ ซึ่งหมายความว่าคลาสที่A
ถือ mutex จะไม่ได้รับตัวสร้างการย้ายเริ่มต้น
ฉันจะทำให้ประเภทนี้A
เคลื่อนย้ายได้ด้วยวิธีที่ปลอดภัยต่อเธรดได้อย่างไร
ตามการออกแบบstd::mutex
ไม่สามารถเคลื่อนย้ายหรือคัดลอกได้ ซึ่งหมายความว่าคลาสที่A
ถือ mutex จะไม่ได้รับตัวสร้างการย้ายเริ่มต้น
ฉันจะทำให้ประเภทนี้A
เคลื่อนย้ายได้ด้วยวิธีที่ปลอดภัยต่อเธรดได้อย่างไร
std::lock_guard
method เท่านั้นที่กำหนดขอบเขตไว้
คำตอบ:
เริ่มต้นด้วยรหัสเล็กน้อย:
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
mutex
เมื่อสร้างคุณไม่จำเป็นต้องล็อค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 หรือ rhs ของนิพจน์การมอบหมายหรือไม่ และโดยทั่วไปคุณต้องป้องกันสถานการณ์ต่อไปนี้:
// 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)
เพื่อล็อค mutex สองตัวแทนที่จะล็อคไว้ทีละอัน หากคุณล็อกทีละรายการจากนั้นเมื่อสองเธรดกำหนดวัตถุสองชิ้นในลำดับตรงข้ามกันดังที่แสดงด้านบนคุณจะได้รับการหยุดชะงัก ประเด็น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>;
นี่อาจเป็นการเพิ่มประสิทธิภาพ แต่ไม่แน่นอน คุณจะต้องวัดผลเพื่อดูว่าเป็น แต่ด้วยการเปลี่ยนแปลงนี้เราสามารถคัดลอกโครงสร้างจาก rhs เดียวกันในหลายเธรดพร้อมกันได้ โซลูชัน 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
สามารถให้ข้อมูลเชิงลึกเกี่ยวกับ API ที่คุณอาจต้องระบุสำหรับ "เธรดปลอดภัย" A
ซึ่งโดยทั่วไปจะแตกต่างจาก API ที่ "ไม่ปลอดภัยเธรด" เนื่องจากปัญหา "ความละเอียดในการล็อก"
โปรดทราบถึงความจำเป็นในการป้องกัน "self-swap" "self-swap" ควรเป็น no-op หากไม่มีการตรวจสอบตัวเองระบบจะล็อก mutex เดิมซ้ำ ๆ สิ่งนี้สามารถแก้ไขได้โดยไม่ต้องตรวจสอบตัวเองโดยใช้std::recursive_mutex
สำหรับMutexType
.
อัปเดต
ในความคิดเห็นด้านล่าง Yakk ค่อนข้างไม่พอใจที่ต้องสร้างสิ่งต่าง ๆ ในสำเนาและย้ายตัวสร้าง (และเขามีประเด็น) หากคุณรู้สึกหนักแน่นเกี่ยวกับปัญหานี้มากพอจนเต็มใจที่จะใช้ความทรงจำกับปัญหานี้คุณสามารถหลีกเลี่ยงได้ดังนี้:
เพิ่มประเภทการล็อกที่คุณต้องการเป็นสมาชิกข้อมูล สมาชิกเหล่านี้ต้องมาก่อนข้อมูลที่ได้รับการปกป้อง:
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();
}
อ๊ะ Yakk ลบความคิดเห็นของเขาก่อนที่ฉันจะมีโอกาสอัปเดตนี้ให้เสร็จสมบูรณ์ แต่เขาก็สมควรได้รับเครดิตในการผลักดันปัญหานี้และหาทางแก้ไขในคำตอบนี้
อัปเดต 2
และ 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
แบ่งประเภทชั้นเรียนไม่ใช่ "ทางเดียวที่แท้จริง" มันเป็นเครื่องมือในกล่องเครื่องมือและหากคุณต้องการใช้มันเป็นอย่างไร
เนื่องจากดูเหมือนจะไม่มีวิธีที่ดีสะอาดและง่ายในการตอบคำถามนี้ - วิธีแก้ปัญหาของ Anton ฉันคิดว่าถูกต้อง แต่เป็นที่ถกเถียงกันอย่างชัดเจนเว้นแต่จะได้คำตอบที่ดีกว่าฉันขอแนะนำให้วางคลาสดังกล่าวไว้ในกองและดูแลมัน ผ่านทางstd::unique_ptr
:
auto a = std::make_unique<A>();
ตอนนี้เป็นประเภทที่เคลื่อนย้ายได้อย่างสมบูรณ์และใครก็ตามที่มีการล็อค mutex ภายในในขณะที่การเคลื่อนไหวเกิดขึ้นก็ยังปลอดภัยแม้ว่าจะเป็นที่ถกเถียงกันว่านี่เป็นสิ่งที่ดีที่ควรทำหรือไม่
หากคุณต้องการคัดลอกความหมายเพียงใช้
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
ออกจากการเป็นเพื่อนแบบอินไลน์เพื่อรับมือกับความสับสนที่เกินพิกัดในกรณีนั้น
การใช้ mutexes และ C ++ move semantics เป็นวิธีที่ยอดเยี่ยมในการถ่ายโอนข้อมูลระหว่างเธรดอย่างปลอดภัยและมีประสิทธิภาพ
ลองนึกภาพเธรด 'ผู้ผลิต' ที่สร้างชุดสตริงและมอบให้กับผู้บริโภค (หนึ่งรายขึ้นไป) สำหรับกระบวนการเหล่านั้นอาจจะเป็นตัวแทนของวัตถุที่มี (ใหญ่อาจ) std::vector<std::string>
วัตถุ เราต้องการ 'ย้าย' สถานะภายในของเวกเตอร์เหล่านั้นไปยังผู้บริโภคอย่างแท้จริงโดยไม่ต้องทำซ้ำโดยไม่จำเป็น
คุณเพียงแค่รับรู้ว่า mutex เป็นส่วนหนึ่งของวัตถุที่ไม่ใช่ส่วนหนึ่งของสถานะของวัตถุ นั่นคือคุณไม่ต้องการย้าย mutex
การล็อกแบบใดที่คุณต้องการขึ้นอยู่กับอัลกอริทึมของคุณหรือการทำให้วัตถุของคุณมีลักษณะทั่วไปและช่วงการใช้งานที่คุณอนุญาต
หากคุณเคยย้ายจากออบเจ็กต์ 'ผู้ผลิต' ที่ใช้ร่วมกันไปยังอ็อบเจ็กต์ 'การใช้งาน' เธรดโลคัลคุณอาจตกลงที่จะล็อกเฉพาะการย้ายจากวัตถุที่
หากเป็นการออกแบบทั่วไปคุณจะต้องล็อกทั้งสองอย่าง ในกรณีเช่นนี้คุณต้องพิจารณาการล็อคตาย
หากนั่นเป็นปัญหาที่อาจเกิดขึ้นstd::lock()
ให้ใช้เพื่อรับการล็อกบน mutexes ทั้งสองด้วยวิธีที่ไม่มีการหยุดชะงัก
http://en.cppreference.com/w/cpp/thread/lock
หมายเหตุสุดท้ายคุณต้องแน่ใจว่าคุณเข้าใจความหมายของการย้าย จำได้ว่าย้ายจากวัตถุถูกปล่อยให้อยู่ในสถานะที่ถูกต้อง แต่ไม่รู้จัก เป็นไปได้ทั้งหมดว่าเธรดที่ไม่ดำเนินการย้ายมีเหตุผลที่ถูกต้องในการพยายามเข้าถึงวัตถุที่ถูกย้ายจากเมื่อพบว่าสถานะที่ถูกต้อง แต่ไม่รู้จัก
อีกครั้งโปรดิวเซอร์ของฉันแค่ต่อสู้กับสตริงและผู้บริโภคก็รับภาระทั้งหมดไป ในกรณีนี้ทุกครั้งที่ผู้ผลิตพยายามเพิ่มเวกเตอร์อาจพบว่าเวกเตอร์ไม่ว่างเปล่าหรือว่างเปล่า
ในระยะสั้นหากการเข้าถึงที่อาจเกิดขึ้นพร้อมกันในการย้ายจากอ็อบเจ็กต์เป็นจำนวนการเขียนก็น่าจะใช้ได้ หากมีจำนวนเท่ากับการอ่านลองคิดดูว่าเหตุใดจึงเป็นเรื่องปกติที่จะอ่านโดยพลการ
ก่อนอื่นต้องมีบางอย่างผิดปกติกับการออกแบบของคุณหากคุณต้องการย้ายวัตถุที่มี mutex
แต่ถ้าคุณตัดสินใจที่จะทำต่อไปคุณต้องสร้าง mutex ใหม่ในตัวสร้างการย้ายนั่นคือ:
// 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
ที่ดูสะอาดกว่าและไม่น่าจะทำให้เกิดความสับสน คำถามที่ดี.