ฉันจะจัดการกับ mutexes ในประเภทที่เคลื่อนย้ายได้ใน C ++ อย่างไร?


86

ตามการออกแบบstd::mutexไม่สามารถเคลื่อนย้ายหรือคัดลอกได้ ซึ่งหมายความว่าคลาสที่Aถือ mutex จะไม่ได้รับตัวสร้างการย้ายเริ่มต้น

ฉันจะทำให้ประเภทนี้Aเคลื่อนย้ายได้ด้วยวิธีที่ปลอดภัยต่อเธรดได้อย่างไร


4
คำถามมีมุมกลับ: การดำเนินการย้ายเองจะปลอดภัยต่อเธรดหรือไม่หรือเพียงพอหรือไม่หากการเข้าถึงอ็อบเจ็กต์อื่นปลอดภัยเธรด
Jonas Schäfer

2
@ paulm นั่นขึ้นอยู่กับการออกแบบจริงๆ ฉันมักจะเห็นคลาสหนึ่งมีตัวแปรสมาชิก mutex จากนั้นมีเพียงstd::lock_guardmethod เท่านั้นที่กำหนดขอบเขตไว้
Cory Kramer

2
@Jonas Wielicki: ตอนแรกฉันคิดว่าการย้ายมันควรจะปลอดภัยด้วย อย่างไรก็ตามไม่ใช่ว่าฉันคิดถึงมันอีกครั้งสิ่งนี้ไม่สมเหตุสมผลมากนักเนื่องจากการย้ายสร้างวัตถุมักจะทำให้สถานะของวัตถุเก่าไม่ถูกต้อง ดังนั้นเธรดอื่น ๆจะต้องไม่สามารถเข้าถึงอ็อบเจ็กต์เก่าได้หากกำลังจะถูกย้าย .. มิฉะนั้นในไม่ช้าพวกเขาอาจเข้าถึงอ็อบเจ็กต์ที่ไม่ถูกต้อง ฉันถูกไหม?
Jack Sabbath

2
โปรดไปที่ลิงค์นี้อาจใช้เต็มได้justsoftwaresolutions.co.uk/threading/…
Ravi Chauhan

1
@Dieter Lücking: ใช่นี่คือแนวคิด .. mutex M ปกป้องคลาส B อย่างไรก็ตามฉันจะเก็บทั้งสองอย่างเพื่อให้มีวัตถุที่ปลอดภัยและสามารถเข้าถึงได้ที่ไหน? ทั้ง M และ B อาจไปที่คลาส A .. และในกรณีนี้คลาส A จะมี Mutex ที่ขอบเขตคลาส
Jack Sabbath

คำตอบ:


105

เริ่มต้นด้วยรหัสเล็กน้อย:

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แล้ว การพูดอย่างเคร่งครัดนี้ไม่จำเป็นสำหรับสมาชิกการย้าย แต่ฉันสมมติว่าคุณต้องการสมาชิกสำเนาด้วย หากที่ไม่กรณีที่มีความจำเป็นที่จะทำให้ mutablemutex

เมื่อสร้างคุณไม่จำเป็นต้องล็อค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_)
    {}

2
ตัวสร้างการคัดลอกของคุณกำหนดเขตข้อมูล แต่จะไม่คัดลอก นั่นหมายความว่าพวกเขาจำเป็นต้องเป็นค่าเริ่มต้นที่สร้างได้ซึ่งเป็นข้อ จำกัด ที่โชคร้าย
Yakk - Adam Nevraumont

@ ยัค: ใช่การmutexesแบ่งประเภทชั้นเรียนไม่ใช่ "ทางเดียวที่แท้จริง" มันเป็นเครื่องมือในกล่องเครื่องมือและหากคุณต้องการใช้มันเป็นอย่างไร
Howard Hinnant

@Yakk: ค้นหาคำตอบของฉันสำหรับสตริง "C ++ 14"
Howard Hinnant

อาขอโทษฉันพลาด C ++ 14 บิตไป
Yakk - Adam Nevraumont

2
คำอธิบายที่ดี @HowardHinnant! ใน C ++ 17 คุณยังสามารถใช้ std :: scoped_lock lock (x.mut_, y_mut_); ด้วยวิธีนี้คุณต้องพึ่งพาการใช้งานเพื่อล็อค mutexes หลายตัวตามลำดับที่เหมาะสม
fen

7

เนื่องจากดูเหมือนจะไม่มีวิธีที่ดีสะอาดและง่ายในการตอบคำถามนี้ - วิธีแก้ปัญหาของ Anton ฉันคิดว่าถูกต้อง แต่เป็นที่ถกเถียงกันอย่างชัดเจนเว้นแต่จะได้คำตอบที่ดีกว่าฉันขอแนะนำให้วางคลาสดังกล่าวไว้ในกองและดูแลมัน ผ่านทางstd::unique_ptr:

auto a = std::make_unique<A>();

ตอนนี้เป็นประเภทที่เคลื่อนย้ายได้อย่างสมบูรณ์และใครก็ตามที่มีการล็อค mutex ภายในในขณะที่การเคลื่อนไหวเกิดขึ้นก็ยังปลอดภัยแม้ว่าจะเป็นที่ถกเถียงกันว่านี่เป็นสิ่งที่ดีที่ควรทำหรือไม่

หากคุณต้องการคัดลอกความหมายเพียงใช้

auto a2 = std::make_shared<A>();

5

นี่คือคำตอบกลับหัว แทนการฝัง "นี้วัตถุจะต้องตรงกัน" เป็นฐานของชนิดแทนที่จะฉีดมันภายใต้ประเภทใด ๆ

คุณจัดการกับวัตถุที่ซิงโครไนซ์แตกต่างกันมาก ปัญหาใหญ่อย่างหนึ่งคือคุณต้องกังวลเกี่ยวกับการชะงักงัน (การล็อกวัตถุหลายชิ้น) โดยพื้นฐานแล้วไม่ควรเป็น "อ็อบเจ็กต์เวอร์ชันเริ่มต้น" ของคุณ: อ็อบเจ็กต์ที่ซิงโครไนซ์มีไว้สำหรับอ็อบเจ็กต์ที่จะเข้าร่วมการแข่งขันและเป้าหมายของคุณควรลดความขัดแย้งระหว่างเธรดให้น้อยที่สุดไม่ใช่กวาดไปใต้พรม

แต่การซิงโครไนซ์วัตถุยังคงมีประโยชน์ แทนที่จะรับช่วงจากซิงโครไนซ์เราสามารถเขียนคลาสที่รวมประเภทโดยพลการในการซิงโครไนซ์ ผู้ใช้ต้องกระโดดผ่านห่วงสองสามครั้งเพื่อดำเนินการกับวัตถุในตอนนี้ที่มีการซิงโครไนซ์ แต่ไม่ จำกัด เฉพาะชุดการดำเนินการที่ จำกัด ด้วยรหัสด้วยมือบนวัตถุ พวกเขาสามารถรวบรวมการดำเนินการหลายอย่างบนวัตถุให้เป็นหนึ่งเดียวหรือมีการดำเนินการกับหลายวัตถุ

นี่คือกระดาษห่อหุ้มที่ซิงโครไนซ์รอบประเภทโดยพลการ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ออกจากการเป็นเพื่อนแบบอินไลน์เพื่อรับมือกับความสับสนที่เกินพิกัดในกรณีนั้น

ตัวอย่างสด


4

การใช้ mutexes และ C ++ move semantics เป็นวิธีที่ยอดเยี่ยมในการถ่ายโอนข้อมูลระหว่างเธรดอย่างปลอดภัยและมีประสิทธิภาพ

ลองนึกภาพเธรด 'ผู้ผลิต' ที่สร้างชุดสตริงและมอบให้กับผู้บริโภค (หนึ่งรายขึ้นไป) สำหรับกระบวนการเหล่านั้นอาจจะเป็นตัวแทนของวัตถุที่มี (ใหญ่อาจ) std::vector<std::string>วัตถุ เราต้องการ 'ย้าย' สถานะภายในของเวกเตอร์เหล่านั้นไปยังผู้บริโภคอย่างแท้จริงโดยไม่ต้องทำซ้ำโดยไม่จำเป็น

คุณเพียงแค่รับรู้ว่า mutex เป็นส่วนหนึ่งของวัตถุที่ไม่ใช่ส่วนหนึ่งของสถานะของวัตถุ นั่นคือคุณไม่ต้องการย้าย mutex

การล็อกแบบใดที่คุณต้องการขึ้นอยู่กับอัลกอริทึมของคุณหรือการทำให้วัตถุของคุณมีลักษณะทั่วไปและช่วงการใช้งานที่คุณอนุญาต

หากคุณเคยย้ายจากออบเจ็กต์ 'ผู้ผลิต' ที่ใช้ร่วมกันไปยังอ็อบเจ็กต์ 'การใช้งาน' เธรดโลคัลคุณอาจตกลงที่จะล็อกเฉพาะการย้ายจากวัตถุที่

หากเป็นการออกแบบทั่วไปคุณจะต้องล็อกทั้งสองอย่าง ในกรณีเช่นนี้คุณต้องพิจารณาการล็อคตาย

หากนั่นเป็นปัญหาที่อาจเกิดขึ้นstd::lock()ให้ใช้เพื่อรับการล็อกบน mutexes ทั้งสองด้วยวิธีที่ไม่มีการหยุดชะงัก

http://en.cppreference.com/w/cpp/thread/lock

หมายเหตุสุดท้ายคุณต้องแน่ใจว่าคุณเข้าใจความหมายของการย้าย จำได้ว่าย้ายจากวัตถุถูกปล่อยให้อยู่ในสถานะที่ถูกต้อง แต่ไม่รู้จัก เป็นไปได้ทั้งหมดว่าเธรดที่ไม่ดำเนินการย้ายมีเหตุผลที่ถูกต้องในการพยายามเข้าถึงวัตถุที่ถูกย้ายจากเมื่อพบว่าสถานะที่ถูกต้อง แต่ไม่รู้จัก

อีกครั้งโปรดิวเซอร์ของฉันแค่ต่อสู้กับสตริงและผู้บริโภคก็รับภาระทั้งหมดไป ในกรณีนี้ทุกครั้งที่ผู้ผลิตพยายามเพิ่มเวกเตอร์อาจพบว่าเวกเตอร์ไม่ว่างเปล่าหรือว่างเปล่า

ในระยะสั้นหากการเข้าถึงที่อาจเกิดขึ้นพร้อมกันในการย้ายจากอ็อบเจ็กต์เป็นจำนวนการเขียนก็น่าจะใช้ได้ หากมีจำนวนเท่ากับการอ่านลองคิดดูว่าเหตุใดจึงเป็นเรื่องปกติที่จะอ่านโดยพลการ


3

ก่อนอื่นต้องมีบางอย่างผิดปกติกับการออกแบบของคุณหากคุณต้องการย้ายวัตถุที่มี 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.
    {
    }
};

สิ่งนี้ปลอดภัยสำหรับเธรดเนื่องจากตัวสร้างการย้ายสามารถสันนิษฐานได้อย่างปลอดภัยว่าอาร์กิวเมนต์ไม่ได้ใช้ที่อื่นดังนั้นจึงไม่จำเป็นต้องล็อกอาร์กิวเมนต์


2
นั่นไม่ใช่เธรดที่ปลอดภัย จะเกิดอะไรขึ้นถ้าa.mutexล็อค: คุณหลุดสถานะนั้น -1

2
@ DieterLückingตราบใดที่อาร์กิวเมนต์เป็นเพียงการอ้างอิงไปยังวัตถุที่ย้ายจากไม่มีเหตุผลที่ดีที่จะล็อก mutex และแม้ว่าจะเป็นเช่นนั้นก็ไม่มีเหตุผลที่จะล็อก mutex ของวัตถุที่สร้างขึ้นใหม่ และถ้ามีนี่เป็นข้อโต้แย้งสำหรับการออกแบบที่ไม่ดีโดยรวมของวัตถุที่เคลื่อนย้ายได้ด้วย mutexes
Anton Savin

1
@ DieterLückingนี่ไม่เป็นความจริง คุณสามารถระบุรหัสที่แสดงปัญหาได้หรือไม่? และไม่อยู่ในรูปแบบA a; A a2(std::move(a)); do some stuff with a.
Anton Savin

2
อย่างไรก็ตามหากนี่เป็นวิธีที่ดีที่สุดฉันขอแนะนำให้newเปิดอินสแตนซ์และวางอินสแตนซ์std::unique_ptrที่ดูสะอาดกว่าและไม่น่าจะทำให้เกิดความสับสน คำถามที่ดี.
Mike Vine

1
@MikeVine ฉันคิดว่าคุณควรเพิ่มเป็นคำตอบ
Anton Savin
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.