คัดลอกคอนสตรัคที่มีอาร์กิวเมนต์ที่ไม่ใช่ const แนะนำโดยกฎความปลอดภัยของเธรด?


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ใน wrapper ฉันเดากฎต่อไปนี้ที่ทันสมัย: 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-function พฤติกรรมจะได้รับการบันทึกไว้เท่านั้น แต่ไม่มีแนวคิดเรื่องความมั่นคง)

ฉันสามารถคิดถึงสถานการณ์ที่เป็นไปได้มากมาย:

(1)ความเป็นไปได้อย่างหนึ่งคือไม่มีวิธีใช้ตัวสร้างสำเนาที่มีความหมายตามปกติเลย (ใช่ฉันสามารถย้ายวัตถุและนั่นไม่ใช่สิ่งที่ฉันต้องการ)

(2)ในทางกลับกันการคัดลอกวัตถุนั้นไม่ปลอดภัยโดยเธรดในแง่ที่ว่าการทำสำเนาแบบง่าย ๆ สามารถค้นหาแหล่งที่มาในสภาพที่ถูกดัดแปลงครึ่งดังนั้นฉันสามารถไปข้างหน้าและทำสิ่งนี้ได้

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)ในที่สุดฉันสามารถทำตามตรรกะและสร้างตัวคัดลอกที่ใช้อาร์กิวเมนต์ที่ไม่ใช่ const

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บริบทเพราะวัตถุเหล่านี้มักจะไม่

คำถามคือมันเป็นเส้นทางที่ถูกต้องหรือทั่วไป?

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

(5)ในที่สุดแม้ว่าสิ่งนี้จะเกินความจริงและอาจมีค่าใช้จ่ายสูงชัน แต่ฉันสามารถเพิ่ม mutex ได้:

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; 
}

แก้ไข 3: ฉันได้เรียนรู้เมื่อเร็ว ๆ นี้ว่าstd::auto_ptrมีปัญหาคล้ายกันในการมีตัวสร้าง "คัดลอก" ที่ไม่ใช่ const ผลคือauto_ptrไม่สามารถใช้ภายในคอนเทนเนอร์ได้ https://www.quantstart.com/articles/STL-Containers-and-Auto_ptrs-Why-They-Dont-Mix/


1
" ในรหัสมรดกนี้ฟังก์ชั่นที่ทำซ้ำวัตถุไม่ปลอดภัยเธรด (เมื่อเรียกอาร์กิวเมนต์แรกเดียวกัน) " คุณแน่ใจหรือไม่? มีบางสถานะที่ไม่มีอยู่ภายในLซึ่งถูกแก้ไขโดยการสร้างLอินสแตนซ์ใหม่หรือไม่? หากไม่เป็นเช่นนั้นคุณเชื่อว่าการดำเนินการนี้ไม่ปลอดภัยสำหรับเธรดหรือไม่
Nicol Bolas

ใช่นั่นคือสถานการณ์ ดูเหมือนว่าสถานะภายในของอาร์กิวเมนต์แรกจะถูกแก้ไขในระหว่างการทดสอบ ด้วยเหตุผลบางอย่าง (บางส่วน "การเพิ่มประสิทธิภาพ" หรือการออกแบบที่ไม่ดีหรือตามข้อกำหนด) ฟังก์ชั่นlegacy_duplicateไม่สามารถเรียกด้วยอาร์กิวเมนต์แรกเดียวกันจากสองเธรดที่แตกต่างกัน
alfC

@TedLyngmo ตกลงฉันทำ แม้ว่าในทางเทคนิคใน c ++ pre 11 const มีความหมายคลุมเครือมากขึ้นในการมีเธรด
alfC

@TedLyngmo ใช่มันเป็นวิดีโอที่ค่อนข้างดี มันเป็นเรื่องที่น่าเสียดายที่วิดีโอนั้นเกี่ยวข้องกับสมาชิกที่เหมาะสมและไม่ได้สัมผัสกับปัญหาของการสร้าง (เช่นเดียวกับความคงอยู่ของวัตถุ "อื่น ๆ ") ในมุมมองอาจไม่มีวิธีที่แท้จริงในการทำให้เธรด wrapper นี้ปลอดภัยเมื่อคัดลอกโดยไม่ต้องเพิ่มเลเยอร์ abstraction อื่น (และ mutex ที่เป็นรูปธรรม)
alfC

ใช่แล้วนั่นทำให้ฉันสับสนและฉันอาจเป็นหนึ่งในคนเหล่านั้นที่ไม่รู้ว่าconstจริง ๆ แล้วหมายถึงอะไร :-) ฉันไม่อยากจะคิดว่าสองครั้งเกี่ยวกับการconst&ใน ctor otherสำเนาของฉันตราบเท่าที่ฉันไม่แก้ไข ฉันมักจะนึกถึงความปลอดภัยของเธรดเป็นสิ่งที่เราจะต้องเข้าถึงได้จากหลาย ๆ หัวข้อผ่านการห่อหุ้มและฉันหวังว่าจะได้คำตอบจริงๆ
Ted Lyngmo

คำตอบ:


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; }(เช่นไม่ใช่ const in)
alfC

คำตอบของคุณน่าสนใจมากเพราะสามารถรวมกับตัวเลือก (4) และตัวเลือกรุ่นที่ชัดเจน (2) นั่นคือA a2(a1)สามารถลองใช้เธรดเซฟ (หรือลบ) และA a2(const_cast<A&>(a1))จะไม่พยายามเซฟเธรดเลย
alfC

2
ใช่ถ้าคุณวางแผนที่จะใช้Aในบริบทของเธรดที่ปลอดภัยและไม่ปลอดภัยของเธรดคุณควรดึงconst_castรหัสการโทรเพื่อให้ชัดเจนว่าที่ใดที่ความปลอดภัยของเธรดทราบว่าถูกละเมิด มันก็โอเคที่จะผลักดันความปลอดภัยเป็นพิเศษที่อยู่เบื้องหลัง API (mutex) แต่ไม่เป็นไรที่จะซ่อนความไม่ปลอดภัย (const_cast)
Michael Graczyk

0

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

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

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

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

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

จากด้านบนคุณมีสองเส้นทางที่คุณสามารถติดตามได้:

A) คุณรู้ว่าการคัดลอกวัตถุนี้จากหลายเธรดจะไม่เกิดขึ้นบ่อยครั้งเพียงพอสำหรับค่าใช้จ่ายในการล็อคเพิ่มเติมที่มีค่าใช้จ่าย - อาจจะมีค่าใช้จ่ายน้อยมากเนื่องจากขั้นตอนการทำซ้ำที่มีอยู่นั้นมีราคาแพง Spinlock / mutex ที่หมุนล่วงหน้าและไม่มีการโต้แย้งใด ๆ

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

ฉันสงสัยว่าคุณอยู่ในสถานการณ์ A จริง ๆ และเพิ่งเพิ่ม mutlock spinlock / Spinning ที่ใกล้เคียงกับไม่มีการลงโทษประสิทธิภาพเมื่อไม่มีการโต้แย้งจะทำงานได้ดี (อย่าลืมอ้างอิงมาตรฐาน)

ในทางทฤษฎีแล้วมีสถานการณ์อื่น:

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

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

เพียงเพิ่ม mutex / spinlock และเกณฑ์มาตรฐาน


คุณช่วยชี้ให้ฉันดูเนื้อหาของ spinlock / pre-Spinning mutex ใน C ++ ได้ไหม มันซับซ้อนกว่าstd::mutexหรือเปล่าที่มีให้โดย? ฟังก์ชั่นที่ซ้ำกันไม่มีความลับฉันไม่ได้พูดถึงมันเพื่อรักษาปัญหาในระดับสูงและไม่ได้รับคำตอบเกี่ยวกับ MPI แต่เมื่อคุณไปที่ลึกฉันสามารถให้รายละเอียดเพิ่มเติม ฟังก์ชั่นแบบเดิมคือMPI_Comm_dupและ Safeness ไม่ใช่หัวข้อที่มีประสิทธิภาพมีการอธิบายไว้ที่นี่ (ผมยืนยันว่า) github.com/pmodels/mpich/issues/3234 นี่คือเหตุผลที่ฉันไม่สามารถแก้ไขสำเนา (นอกจากนี้หากฉันเพิ่ม mutex ฉันจะถูกล่อลวงให้ทำ MPI ทั้งหมดที่เรียกว่า thread-safe)
alfC

น่าเศร้าที่ฉันไม่รู้จัก 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

ขอบคุณอีกครั้งฉันอยู่ใน Linux ถ้าเรื่องนั้น
alfC

นี่คือการเปรียบเทียบประสิทธิภาพโดยละเอียด (สำหรับภาษาอื่น แต่ฉันคิดว่านี่เป็นข้อมูลและบ่งบอกถึงสิ่งที่คาดหวัง): matklad.github.io/2020/01/04/ ...... TLDR คือ - spinlock ที่ชนะโดยขนาดเล็กมาก ระยะขอบเมื่อไม่มีการช่วงชิงสามารถสูญเสียอย่างรุนแรงเมื่อมีข้อขัดแย้ง
DeducibleSteak
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.