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 const
dalam pembungkus. Saya kira mengikuti aturan modern: https://herbsutter.com/2013/01/01/video-you-dont-know-const-and-mutable/
Ini duplicate
terlihat 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_duplicate
bukan 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 duplicate
const dan kebohongan tentang keamanan utas dalam semua konteks. (Setelah semua fungsi warisan tidak peduli const
sehingga 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_duplicate
dari 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;
}
L
yang dimodifikasi dengan membuat yang baruL
instance ? Jika tidak, mengapa Anda percaya bahwa operasi ini tidak aman?legacy_duplicate
tidak dapat dipanggil dengan argumen pertama yang sama dari dua utas yang berbeda.const
sebenarnya berarti. :-) Saya tidak akan berpikir dua kali untuk mengambilconst&
ctor copy saya selama saya tidak memodifikasiother
. 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.Jawaban:
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.
Keluaran:
Ini mengikuti panduan gaya Google di mana
const
mengomunikasikan keamanan utas, tetapi pemanggilan kode API Anda dapat menyisih menggunakanconst_cast
sumber
legacy_duplicate
bisavoid 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-constin
)A a2(a1)
bisa mencoba menjadi utas aman (atau dihapus) danA a2(const_cast<A&>(a1))
tidak akan mencoba menjadi utas aman sama sekali.A
dalam konteks utas aman dan utas tidak aman, Anda harus menariknyaconst_cast
ke 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).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.
sumber
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 adalahMPI_Comm_dup
dan 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.)