Salin konstruktor dengan argumen non-const yang disarankan oleh aturan utas keselamatan?

9

Saya punya pembungkus untuk beberapa kode warisan.

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

Dalam kode lawas ini, fungsi yang "menduplikasi" suatu objek bukanlah thread aman (saat memanggil argumen pertama yang sama), oleh karena itu tidak ditandai constdalam pembungkus. Saya kira mengikuti aturan modern: https://herbsutter.com/2013/01/01/video-you-dont-know-const-and-mutable/

Ini duplicateterlihat seperti cara yang baik untuk mengimplementasikan copy constructor, kecuali untuk detail yang bukan const. Karena itu saya tidak dapat melakukan ini secara langsung:

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

Jadi apa jalan keluar dari situasi paradoks ini?

(Katakan juga itu legacy_duplicatebukan thread-safe tapi saya tahu meninggalkan objek dalam keadaan asli ketika keluar. Sebagai fungsi-C perilaku hanya didokumentasikan tetapi tidak memiliki konsep keteguhan.)

Saya bisa memikirkan banyak skenario yang mungkin:

(1) Satu kemungkinan adalah bahwa tidak ada cara untuk mengimplementasikan copy constructor dengan semantik yang biasa sama sekali. (Ya, saya bisa memindahkan objek dan bukan itu yang saya butuhkan.)

(2) Di sisi lain, menyalin suatu objek secara inheren non-thread-safe dalam arti bahwa menyalin tipe sederhana dapat menemukan sumber dalam keadaan setengah dimodifikasi, jadi saya bisa maju dan melakukan ini mungkin,

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) atau bahkan hanya menyatakan duplicateconst dan kebohongan tentang keamanan utas dalam semua konteks. (Setelah semua fungsi warisan tidak peduli constsehingga kompiler tidak akan mengeluh.)

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

(4) Akhirnya, saya bisa mengikuti logika dan membuat copy-constructor yang membutuhkan argumen non-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;}
};

Ternyata ini bekerja dalam banyak konteks, karena objek ini biasanya tidak const.

Pertanyaannya adalah, apakah ini rute yang valid atau umum?

Saya tidak dapat menyebutkan nama mereka, tetapi secara intuitif saya berharap banyak masalah di jalan memiliki konstruktor salinan non-const. Mungkin itu tidak akan memenuhi syarat sebagai tipe nilai karena kehalusan ini.

(5) Akhirnya, meskipun ini tampaknya berlebihan dan bisa memiliki biaya runtime yang curam, saya bisa menambahkan 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;
};

Tetapi dipaksa untuk melakukan ini tampak seperti pesimisasi dan membuat kelas lebih besar. Saya tidak yakin. Saat ini saya condong ke arah (4) , atau (5) atau kombinasi keduanya.

—— EDIT

Pilihan lain:

(6) Lupakan semua non-sense dari fungsi anggota duplikat dan cukup panggil legacy_duplicatedari konstruktor dan nyatakan bahwa konstruktor salinan tidak aman utas. (Dan jika perlu, buat versión lain yang aman untuk jenis ini, A_mt)

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

EDIT 2

Ini bisa menjadi model yang baik untuk apa fungsi legacy tidak. Perhatikan bahwa dengan menyentuh input, panggilan tidak aman untuk nilai yang diwakili oleh argumen pertama.

void legacy_duplicate(L* in, L** out){
   *out = new L{};
   char tmp = in[0];
   in[0] = tmp; 
   std::memcpy(*out, in, sizeof *in); return; 
}
alfC
sumber
1
" Dalam kode warisan ini fungsi yang menduplikasi objek tidak aman utas (saat memanggil argumen pertama yang sama) " Apakah Anda yakin tentang itu? Apakah ada beberapa negara yang tidak ada di dalamnya Lyang dimodifikasi dengan membuat yang baruL instance ? Jika tidak, mengapa Anda percaya bahwa operasi ini tidak aman?
Nicol Bolas
Ya, itulah situasinya. Sepertinya keadaan internal argumen pertama dimodifikasi selama exection. Untuk beberapa alasan (beberapa "optimasi" atau desain yang buruk atau hanya dengan spesifikasi) fungsi legacy_duplicatetidak dapat dipanggil dengan argumen pertama yang sama dari dua utas yang berbeda.
alfC
@ TedLyngmo ok saya lakukan. Meskipun secara teknis di c ++ pre 11 const memiliki makna yang lebih kabur di hadapan utas.
alfC
@TedLyngmo ya, ini adalah video yang cukup bagus. sangat disayangkan bahwa video hanya berurusan dengan anggota yang tepat dan tidak menyentuh masalah konstruksi (juga ketegasan pada objek "lain"). Dalam perspektif mungkin tidak ada cara intrinsik untuk membuat utas pembungkus ini aman setelah disalin tanpa menambahkan lapisan abstraksi lain (dan mutex beton).
alfC
Ya, itu membuat saya bingung dan saya mungkin salah satu dari orang-orang yang tidak tahu apa yang constsebenarnya berarti. :-) Saya tidak akan berpikir dua kali untuk mengambil const&ctor copy saya selama saya tidak memodifikasi other. Saya selalu menganggap keamanan utas sebagai sesuatu yang ditambahkan di atas apa pun yang perlu diakses dari banyak utas, melalui enkapsulasi, dan saya benar-benar menantikan jawaban.
Ted Lyngmo

Jawaban:

0

Saya hanya akan menyertakan opsi Anda (4) dan (5), tetapi secara eksplisit memilih untuk melakukan perilaku tidak aman saat Anda merasa perlu untuk kinerja.

Ini adalah contoh lengkapnya.

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

Keluaran:

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

Ini mengikuti panduan gaya Google di mana constmengomunikasikan keamanan utas, tetapi pemanggilan kode API Anda dapat menyisih menggunakanconst_cast

Michael Graczyk
sumber
Terima kasih atas jawabannya, saya pikir itu tidak mengubah asnwer Anda dan saya tidak yakin tetapi model yang lebih baik untuk legacy_duplicatebisa 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; }(yaitu non-const in)
alfC
Jawaban Anda sangat menarik karena dapat dikombinasikan dengan opsi (4) dan versi eksplisit opsi (2). Artinya, A a2(a1)bisa mencoba menjadi utas aman (atau dihapus) dan A a2(const_cast<A&>(a1))tidak akan mencoba menjadi utas aman sama sekali.
alfC
2
Ya, jika Anda berencana untuk menggunakan Adalam konteks utas aman dan utas tidak aman, Anda harus menariknya const_castke kode panggilan sehingga jelas di mana keamanan utas diketahui dilanggar. Tidak apa-apa untuk mendorong keamanan ekstra di belakang API (mutex) tetapi tidak oke untuk menyembunyikan ketidakamanan (const_cast).
Michael Graczyk
0

TLDR: Baik perbaiki implementasi fungsi duplikasi Anda, atau perkenalkan mutex (atau perangkat penguncian yang lebih tepat, mungkin spinlock, atau pastikan mutex Anda dikonfigurasi untuk berputar sebelum melakukan hal yang lebih berat) untuk saat ini , kemudian perbaiki implementasi duplikasi dan lepaskan penguncian saat penguncian benar-benar menjadi masalah.

Saya pikir poin utama yang perlu diperhatikan adalah bahwa Anda menambahkan fitur yang tidak ada sebelumnya: kemampuan untuk menduplikasi objek dari banyak utas pada saat yang sama.

Jelas, dalam kondisi yang telah Anda deskripsikan, itu akan menjadi bug - kondisi ras, jika Anda telah melakukan itu sebelumnya, tanpa menggunakan semacam sinkronisasi eksternal.

Karenanya, segala penggunaan fitur baru ini akan menjadi sesuatu yang Anda tambahkan ke kode Anda, bukan sebagai fungsi bawaan yang ada. Anda harus menjadi orang yang tahu apakah menambahkan penguncian tambahan sebenarnya akan mahal - tergantung pada seberapa sering Anda akan menggunakan fitur baru ini.

Juga, berdasarkan kompleksitas objek yang dipersepsikan - oleh perlakuan khusus yang Anda berikan, saya akan menganggap bahwa prosedur duplikasi bukan yang sepele, oleh karena itu, sudah cukup mahal dalam hal kinerja.

Berdasarkan hal di atas, Anda memiliki dua jalur yang dapat Anda ikuti:

A) Anda tahu bahwa menyalin objek ini dari banyak utas tidak akan cukup sering terjadi sehingga overhead dari penguncian tambahan menjadi mahal - mungkin murah, setidaknya mengingat bahwa prosedur duplikasi yang ada cukup mahal sendiri, jika Anda menggunakan spinlock / pre-spinning mutex, dan tidak ada anggapan tentang itu.

B) Anda menduga bahwa penyalinan dari banyak utas akan cukup sering terjadi sehingga penguncian ekstra menjadi masalah. Maka Anda hanya memiliki satu opsi - perbaiki kode duplikasi Anda. Jika Anda tidak memperbaikinya, Anda tetap harus mengunci, apakah pada lapisan abstraksi ini atau di tempat lain, tetapi Anda akan membutuhkannya jika Anda tidak ingin bug - dan seperti yang telah kami buat, di jalur ini, Anda menganggap bahwa penguncian akan terlalu mahal, oleh karena itu, satu-satunya pilihan adalah memperbaiki kode duplikasi.

Saya menduga bahwa Anda benar-benar dalam situasi A, dan hanya menambahkan spinlock / spinning mutex yang hampir tidak ada penalti kinerja ketika tidak terbantahkan, akan bekerja dengan baik (ingat untuk membandingkannya, meskipun).

Secara teori, ada situasi lain:

C) Berbeda dengan kompleksitas yang tampak dari fungsi duplikasi, itu sebenarnya sepele, tetapi tidak dapat diperbaiki karena alasan tertentu; itu sangat sepele sehingga bahkan spinlock yang tidak terbantahkan menghadirkan penurunan kinerja yang tidak dapat diterima terhadap duplikasi; duplikasi pada utas paralel jarang digunakan; duplikasi pada satu utas digunakan sepanjang waktu, membuat penurunan kinerja benar-benar tidak dapat diterima.

Dalam hal ini, saya menyarankan yang berikut: nyatakan konstruktor / operator salin default dihapus, untuk mencegah siapa pun menggunakannya secara tidak sengaja. Buat dua metode duplikasi yang dapat dipanggil secara eksplisit, yang aman utas, dan utas tidak aman; membuat pengguna Anda memanggil mereka secara eksplisit, tergantung pada konteksnya. Sekali lagi, tidak ada cara lain untuk mencapai kinerja single thread yang dapat diterima dan multi-threading aman, jika Anda benar-benar berada dalam situasi ini dan Anda tidak bisa memperbaiki implementasi duplikasi yang ada. Tetapi saya merasa sangat tidak mungkin Anda benar-benar seperti itu.

Tambahkan saja mutex / spinlock dan benchmark.

DeducibleSteak
sumber
Bisakah Anda mengarahkan saya ke materi tentang spinlock / pre-spinning mutex di C ++? Apakah ini sesuatu yang lebih rumit dari apa yang disediakan oleh std::mutex? Fungsi duplikat bukan rahasia, saya tidak menyebutkannya untuk menjaga masalah pada tingkat tinggi dan tidak menerima jawaban tentang MPI. Tapi karena Anda sudah sejauh itu, saya bisa memberi Anda lebih banyak detail. Fungsi legacy adalah MPI_Comm_dupdan keamanan non-thread yang efektif dijelaskan di sini (saya konfirmasikan) github.com/pmodels/mpich/issues/3234 . Inilah sebabnya saya tidak bisa memperbaiki duplikat. (Juga, jika saya menambahkan mutex, saya akan tergoda untuk membuat semua panggilan MPI aman.)
alfC
Sayangnya saya tidak tahu banyak std :: mutex, tapi saya kira itu berputar sebelum membiarkan proses tidur. Perangkat sinkronisasi yang terkenal di mana Anda dapat mengontrol ini secara manual, adalah: docs.microsoft.com/en-us/windows/win32/api/synchapi/... Saya belum membandingkan kinerja, tetapi tampaknya std :: mutex adalah sekarang unggul: stackoverflow.com/questions/9997473/… dan diimplementasikan menggunakan: docs.microsoft.com/en-us/windows/win32/sync/…
DeducibleSteak
Tampaknya ini adalah deskripsi yang baik dari pertimbangan umum untuk dipertimbangkan: stackoverflow.com/questions/5869825/…
DeducibleSteak
Terima kasih lagi, saya di Linux jika itu penting.
alfC
Berikut ini adalah perbandingan kinerja yang agak terperinci (untuk bahasa yang berbeda, tapi saya kira ini informatif dan menunjukkan apa yang diharapkan): matklad.github.io/2020/01/04/... TLDR adalah - spinlocks dimenangkan oleh yang sangat kecil Margin ketika tidak ada pertengkaran, bisa kehilangan buruk ketika ada pertengkaran.
DeducibleSteak