Saya mencoba menjawab ini sendiri, setelah melalui berbagai sumber online (misalnya, yang ini dan yang ini ), Standar C ++ 11, serta jawaban yang diberikan di sini.
Pertanyaan terkait digabungkan (misalnya, " mengapa! Diharapkan? " Digabungkan dengan "mengapa menempatkan bandingkan_exchange_weak () dalam satu lingkaran? ") Dan jawaban diberikan sesuai dengan itu.
Mengapa bandingkan_exchange_weak () harus berada dalam satu lingkaran di hampir semua penggunaan?
Pola Khas A
Anda perlu mencapai pembaruan atom berdasarkan nilai dalam variabel atom. Kegagalan menunjukkan bahwa variabel tidak diperbarui dengan nilai yang kami inginkan dan kami ingin mencobanya kembali. Perhatikan bahwa kami tidak terlalu peduli apakah ini gagal karena penulisan bersamaan atau kegagalan palsu. Tapi kami sangat peduli bahwa kamilah yang membuat perubahan ini.
expected = current.load();
do desired = function(expected);
while (!current.compare_exchange_weak(expected, desired));
Contoh dunia nyata adalah untuk beberapa utas menambahkan elemen ke daftar tertaut tunggal secara bersamaan. Setiap utas memuat penunjuk kepala terlebih dahulu, mengalokasikan node baru dan menambahkan kepala ke node baru ini. Akhirnya, ia mencoba menukar simpul baru dengan kepala.
Contoh lain adalah mengimplementasikan mutex menggunakan std::atomic<bool>
. Paling banyak satu utas dapat masuk ke bagian kritis pada satu waktu, bergantung pada utas mana yang pertama kali disetel current
ke true
dan keluar dari loop.
Pola Khas B
Ini sebenarnya pola yang disebutkan dalam buku Anthony. Berlawanan dengan pola A, Anda ingin variabel atom diperbarui satu kali, tetapi Anda tidak peduli siapa yang melakukannya. Selama belum diupdate, coba lagi. Ini biasanya digunakan dengan variabel boolean. Misalnya, Anda perlu mengimplementasikan pemicu agar mesin status dapat bergerak. Benang mana yang menarik pelatuknya tidak peduli.
expected = false;
while (!current.compare_exchange_weak(expected, true) && !expected);
Perhatikan bahwa kita biasanya tidak dapat menggunakan pola ini untuk mengimplementasikan mutex. Jika tidak, beberapa utas mungkin berada di dalam bagian kritis pada saat yang bersamaan.
Karena itu, seharusnya jarang digunakan di compare_exchange_weak()
luar loop. Sebaliknya, ada kasus dimana versi kuat sedang digunakan. Misalnya,
bool criticalSection_tryEnter(lock)
{
bool flag = false;
return lock.compare_exchange_strong(flag, true);
}
compare_exchange_weak
tidak tepat di sini karena ketika kembali karena kegagalan palsu, kemungkinan besar belum ada yang menempati bagian kritis.
Benang Kelaparan?
Satu hal yang perlu disebutkan adalah apa yang terjadi jika kegagalan palsu terus terjadi sehingga membuat utas kelaparan? Secara teoritis hal itu bisa terjadi pada platform bila compare_exchange_XXX()
diimplementasikan sebagai urutan instruksi (misalnya, LL / SC). Akses yang sering dari baris cache yang sama antara LL dan SC akan menghasilkan kegagalan palsu yang terus menerus. Contoh yang lebih realistis adalah karena penjadwalan yang bodoh di mana semua utas bersamaan disisipkan dengan cara berikut.
Time
| thread 1 (LL)
| thread 2 (LL)
| thread 1 (compare, SC), fails spuriously due to thread 2's LL
| thread 1 (LL)
| thread 2 (compare, SC), fails spuriously due to thread 1's LL
| thread 2 (LL)
v ..
Bisakah itu terjadi?
Untungnya, itu tidak akan terjadi selamanya, berkat apa yang dibutuhkan C ++ 11:
Implementasi harus memastikan bahwa operasi perbandingan-dan-pertukaran yang lemah tidak secara konsisten mengembalikan nilai salah kecuali jika objek atom memiliki nilai yang berbeda dari yang diharapkan atau ada modifikasi bersamaan pada objek atom.
Mengapa kita repot-repot menggunakan bandingkan_exchange_weak () dan menulis loop sendiri? Kami hanya dapat menggunakan bandingkan_exchange_strong ().
Tergantung.
Kasus 1: Saat keduanya perlu digunakan di dalam loop. C ++ 11 mengatakan:
Saat bandingkan-dan-tukar dalam satu putaran, versi yang lemah akan menghasilkan kinerja yang lebih baik pada beberapa platform.
Pada x86 (setidaknya saat ini. Mungkin akan menggunakan skema yang sama seperti LL / SC suatu hari nanti untuk kinerja ketika lebih banyak inti diperkenalkan), versi lemah dan kuat pada dasarnya sama karena keduanya bermuara pada satu instruksi cmpxchg
. Pada beberapa platform lain di mana compare_exchange_XXX()
tidak diimplementasikan secara atomik (di sini berarti tidak ada perangkat keras primitif yang ada), versi lemah di dalam loop dapat memenangkan pertarungan karena yang kuat harus menangani kegagalan palsu dan mencoba lagi sesuai dengan itu.
Tapi,
jarang, kita dapat memilih compare_exchange_strong()
lebih compare_exchange_weak()
bahkan dalam satu lingkaran. Misalnya, ketika ada banyak hal yang harus dilakukan antara variabel atom yang dimuat dan nilai baru yang dihitung dipertukarkan (lihat di function()
atas). Jika variabel atom itu sendiri tidak sering berubah, kita tidak perlu mengulangi perhitungan yang mahal untuk setiap kegagalan palsu. Sebaliknya, kita mungkin berharap bahwa compare_exchange_strong()
"menyerap" kegagalan tersebut dan kita hanya mengulangi perhitungan jika gagal karena perubahan nilai riil.
Kasus 2: Ketika hanya compare_exchange_weak()
perlu digunakan di dalam loop. C ++ 11 juga mengatakan:
Ketika perbandingan-dan-pertukaran yang lemah akan membutuhkan loop dan yang kuat tidak akan, yang kuat lebih disukai.
Ini biasanya terjadi ketika Anda melakukan loop hanya untuk menghilangkan kegagalan palsu dari versi yang lemah. Anda mencoba lagi hingga pertukaran berhasil atau gagal karena penulisan bersamaan.
expected = false;
while (!current.compare_exchange_weak(expected, true) && !expected);
Paling banter, itu menciptakan kembali roda dan melakukan hal yang sama compare_exchange_strong()
. Lebih buruk? Pendekatan ini gagal memanfaatkan sepenuhnya mesin yang menyediakan perangkat keras untuk membandingkan dan menukar yang tidak palsu .
Terakhir, jika Anda mengulang untuk hal lain (mis., Lihat "Pola Khas A" di atas), maka ada peluang bagus yang compare_exchange_strong()
juga akan dimasukkan ke dalam perulangan, yang membawa kita kembali ke kasus sebelumnya.