Apa yang dijamin dengan C ++ std :: atomic di tingkat programmer?

9

Saya telah mendengarkan dan membaca beberapa artikel, pembicaraan, dan pertanyaan tentang stackoverflow std::atomic, dan saya ingin memastikan bahwa saya telah memahaminya dengan baik. Karena saya masih agak bingung dengan garis cache menulis visibilitas karena kemungkinan keterlambatan protokol koherensi cache MESI (atau diturunkan), menyimpan buffer, antrian tidak valid, dan sebagainya.

Saya membaca x86 memiliki model memori yang lebih kuat, dan bahwa jika invalidasi cache tertunda x86 dapat mengembalikan operasi yang dimulai. Tapi saya sekarang hanya tertarik pada apa yang saya anggap sebagai programmer C ++, terlepas dari platform.

[T1: thread1 T2: thread2 V1: variabel atom bersama]

Saya mengerti bahwa std :: atomic menjamin bahwa,

(1) Tidak ada ras data yang terjadi pada variabel (berkat akses eksklusif ke baris cache).

(2) Bergantung pada memory_order mana yang kami gunakan, itu menjamin (dengan penghalang) bahwa konsistensi berurutan terjadi (sebelum penghalang, setelah penghalang atau keduanya).

(3) Setelah penulisan atom (V1) pada T1, atom RMW (V1) pada T2 akan koheren (garis cache-nya akan diperbarui dengan nilai tertulis pada T1).

Tetapi sebagai primer koherensi cache menyebutkan,

Implikasi dari semua hal ini adalah, secara default, banyak yang dapat mengambil data basi (jika permintaan invalidasi yang sesuai berada di antrian invalidation)

Jadi, apakah yang berikut ini benar?

(4) std::atomicTIDAK menjamin bahwa T2 tidak akan membaca nilai 'basi' pada pembacaan atom (V) setelah penulisan atom (V) pada T1.

Pertanyaan jika (4) benar: jika penulisan atom pada T1 membatalkan garis cache tidak peduli penundaan, mengapa T2 menunggu pembatalan efektif ketika operasi atom RMW tetapi tidak pada atom membaca?

Pertanyaan jika (4) salah: kapan thread dapat membaca nilai 'basi' dan "itu terlihat" dalam eksekusi, lalu?

Saya sangat menghargai jawaban Anda

Perbarui 1

Jadi sepertinya saya salah pada (3) saat itu. Bayangkan interleave berikut, untuk V1 awal = 0:

T1: W(1)
T2:      R(0) M(++) W(1)

Meskipun RM2 T2 dijamin terjadi sepenuhnya setelah W (1) dalam kasus ini, TW masih bisa membaca nilai 'basi' (saya salah). Menurut ini, atom tidak menjamin koherensi cache penuh, hanya konsistensi berurutan.

Perbarui 2

(5) Sekarang bayangkan contoh ini (x = y = 0 dan bersifat atomik):

T1: x = 1;
T2: y = 1;
T3: if (x==1 && y==0) print("msg");

sesuai dengan apa yang telah kita bicarakan, melihat "msg" yang ditampilkan di layar tidak akan memberi kita informasi di luar T2 yang dieksekusi setelah T1. Jadi salah satu dari eksekusi berikut mungkin terjadi:

  • T1 <T3 <T2
  • T1 <T2 <T3 (di mana T3 melihat x = 1 tetapi belum y = 1)

Apakah itu benar?

(6) Jika utas selalu dapat membaca nilai 'basi', apa yang akan terjadi jika kami mengambil skenario "publikasikan" tetapi alih-alih memberi isyarat bahwa beberapa data siap, kami melakukan yang sebaliknya (menghapus data)?

T1: delete gameObjectPtr; is_enabled.store(false, std::memory_order_release);
T2: while (is_enabled.load(std::memory_order_acquire)) gameObjectPtr->doSomething();

di mana T2 masih akan menggunakan ptr yang dihapus sampai melihat bahwa is_enabled adalah salah.

(7) Juga, fakta bahwa utas dapat membaca nilai 'basi' berarti bahwa mutex tidak dapat diimplementasikan hanya dengan satu atom bebas kunci, bukan? Itu akan membutuhkan mekanisme sinkronisasi antara utas. Apakah itu membutuhkan atom yang dapat dikunci?

Albert Caldas
sumber

Jawaban:

3
  1. Ya, tidak ada ras data
  2. Ya, dengan tepat memory_order nilai yang Anda dapat menjamin konsistensi berurutan
  3. Baca-modifikasi-tulis atom akan selalu terjadi sebelum atau seluruhnya setelah penulisan atom ke variabel yang sama
  4. Ya, T2 dapat membaca nilai basi dari variabel setelah penulisan atom pada T1

Operasi baca-modifikasi-tulis atom ditentukan dengan cara menjamin keasliannya. Jika utas lain dapat menulis ke nilai setelah pembacaan awal dan sebelum penulisan operasi RMW, maka operasi itu tidak akan menjadi atom.

Utas selalu dapat membaca nilai basi, kecuali saat terjadi - sebelum menjamin pemesanan relatif .

Jika operasi RMW membaca nilai "basi", maka itu menjamin bahwa penulisan yang dihasilkannya akan terlihat sebelum ada tulisan dari utas lain yang akan menimpa nilai yang dibacanya.

Perbarui misalnya

Jika T1 menulis x=1dan T2 tidak x++, dengan xawalnya 0, pilihan dari sudut pandang penyimpanan xadalah:

  1. Tulisan T1 adalah yang pertama, jadi T1 menulis x=1, kemudian T2 membaca x==1, menambahnya menjadi 2 dan menulis kembali x=2sebagai operasi atom tunggal.

  2. Tulisan T1 adalah yang kedua. T2 membaca x==0, menambahnya menjadi 1, dan menulis kembali x=1sebagai operasi tunggal, kemudian T1 menulis x=1.

Namun, asalkan tidak ada titik lain sinkronisasi antara kedua utas ini, utas dapat melanjutkan dengan operasi yang tidak memerah ke memori.

Dengan demikian T1 dapat mengeluarkan x=1, kemudian melanjutkan dengan hal-hal lain, meskipun T2 masih akan membaca x==0(dan dengan demikian menulis x=1).

Jika ada titik sinkronisasi lainnya maka akan menjadi jelas utas mana yang dimodifikasi x terlebih dahulu, karena titik-titik sinkronisasi tersebut akan memaksa pesanan.

Ini paling jelas jika Anda memiliki persyaratan pada nilai yang dibaca dari operasi RMW.

Perbarui 2

  1. Jika Anda menggunakan memory_order_seq_cst(default) untuk semua operasi atom Anda tidak perlu khawatir tentang hal semacam ini. Dari sudut pandang program, jika Anda melihat "msg" lalu T1 berlari, lalu T3, lalu T2.

Jika Anda menggunakan urutan memori lain (terutama memory_order_relaxed) maka Anda dapat melihat skenario lain dalam kode Anda.

  1. Dalam hal ini, Anda memiliki bug. Misalkan is_enabledbendera benar, ketika T2 memasuki whilelingkarannya, sehingga memutuskan untuk menjalankan tubuh. T1 sekarang menghapus data, dan T2 kemudian menunjuk pointer, yang merupakan pointer menggantung, dan perilaku yang tidak jelas terjadi kemudian. Atom tidak membantu atau menghalangi dengan cara apa pun selain mencegah ras data pada bendera.

  2. Anda dapat menerapkan mutex dengan variabel atom tunggal.

Anthony Williams
sumber
Terima kasih banyak @Anthony Wiliams atas jawaban cepat Anda. Saya telah memperbarui pertanyaan saya dengan contoh RMW yang membaca nilai 'basi'. Melihat contoh ini, apa yang Anda maksud dengan pemesanan relatif dan bahwa T2's W (1) akan terlihat sebelum ada tulisan? Apakah ini berarti bahwa begitu T2 melihat perubahan T1, ia tidak akan membaca T2 W (1) lagi?
Albert Caldas
Jadi jika "Threads selalu dapat membaca nilai basi" itu berarti bahwa koherensi cache tidak pernah dijamin (setidaknya pada level c ++ programmer). Bisakah Anda melihat pembaruan2 saya?
Albert Caldas
Sekarang saya melihat bahwa saya seharusnya lebih memperhatikan bahasa dan model memori perangkat keras untuk sepenuhnya memahami semua itu, itu adalah bagian yang saya lewatkan. Terima kasih banyak!
Albert Caldas
1

Mengenai (3) - itu tergantung pada urutan memori yang digunakan. Jika keduanya, toko dan operasi RMW digunakan std::memory_order_seq_cst, maka kedua operasi dipesan dalam beberapa cara - yaitu, toko terjadi sebelum RMW, atau sebaliknya. Jika toko dipesan sebelum RMW, maka dijamin bahwa operasi RMW "melihat" nilai yang disimpan. Jika toko dipesan setelah RMW, itu akan menimpa nilai yang ditulis oleh operasi RMW.

Jika Anda menggunakan perintah memori yang lebih santai, modifikasi akan tetap dipesan dengan cara tertentu (urutan modifikasi variabel), tetapi Anda tidak memiliki jaminan apakah RMW "melihat" nilai dari operasi store - bahkan jika operasi RMW adalah urutan setelah penulisan dalam urutan modifikasi variabel.

Jika Anda ingin membaca artikel lain saya dapat merujuk Anda ke Model Memori untuk C / C ++ Programmer .

mpoeter
sumber
Terima kasih untuk artikelnya, saya belum membacanya. Sekalipun sudah cukup tua, berguna untuk menyatukan ide-ide saya.
Albert Caldas
1
Senang mendengarnya - artikel ini adalah bab yang sedikit diperpanjang dan direvisi dari tesis master saya. :-) Ini berfokus pada model memori seperti yang diperkenalkan C ++ 11; Saya mungkin memperbaruinya untuk mencerminkan perubahan (kecil) yang diperkenalkan di C ++ 14/17. Harap beri tahu saya jika Anda memiliki komentar atau saran untuk perbaikan!
mpoeter