Kapan menggunakan volatile dengan multi threading?

131

Jika ada dua utas yang mengakses variabel global, maka banyak tutorial mengatakan membuat variabel tidak stabil untuk mencegah kompiler melakukan caching variabel dalam register dan karenanya tidak diperbarui dengan benar. Namun dua utas yang mengakses variabel bersama adalah sesuatu yang membutuhkan perlindungan melalui mutex bukan? Tetapi dalam kasus itu, antara penguncian utas dan melepaskan mutex kode berada di bagian kritis di mana hanya satu utas yang dapat mengakses variabel, dalam hal mana variabel tidak perlu volatil?

Jadi, apa gunanya / tujuan volatile dalam program multi-threaded?

David Preston
sumber
3
Dalam beberapa kasus, Anda tidak ingin / butuh perlindungan oleh mutex.
Stefan Mai
4
Terkadang tidak masalah untuk memiliki kondisi balapan, terkadang tidak. Bagaimana Anda menggunakan variabel ini?
David Heffernan
3
@ David: Contoh kapan "boleh" untuk berlomba?
John Dibling
6
@ John Ini dia. Bayangkan Anda memiliki utas pekerja yang memproses sejumlah tugas. Utas pekerja menambah penghitung setiap kali menyelesaikan tugas. Master thread secara berkala membaca penghitung ini dan memperbarui pengguna dengan berita kemajuan. Selama penghitung tersebut disejajarkan dengan benar untuk menghindari robek, tidak perlu menyinkronkan akses. Meskipun ada perlombaan, itu tidak berbahaya.
David Heffernan
5
@ John Perangkat keras tempat kode ini dijalankan menjamin bahwa variabel yang disejajarkan tidak dapat dirusak. Jika pekerja memperbarui n ke n +1 saat pembaca membaca, pembaca tidak peduli apakah mereka mendapatkan n atau n +1. Tidak ada keputusan penting yang akan diambil karena hanya digunakan untuk pelaporan kemajuan.
David Heffernan

Jawaban:

168

Jawaban singkat & cepat : volatilehampir tidak berguna untuk pemrograman platform-agnostik, multithreaded. Itu tidak menyediakan sinkronisasi apa pun, tidak membuat pagar memori, juga tidak memastikan urutan pelaksanaan operasi. Itu tidak membuat operasi atom. Itu tidak membuat kode Anda utas aman secara ajaib. volatilemungkin fasilitas paling banyak disalahpahami di semua C ++. Lihat ini , ini dan ini untuk informasi lebih lanjut tentangvolatile

Di sisi lain, volatilememang ada beberapa kegunaan yang mungkin tidak begitu jelas. Itu dapat digunakan banyak dengan cara yang sama akan digunakan constuntuk membantu kompiler menunjukkan di mana Anda mungkin membuat kesalahan dalam mengakses beberapa sumber daya bersama dengan cara yang tidak dilindungi. Penggunaan ini dibahas oleh Alexandrescu dalam artikel ini . Namun, ini pada dasarnya menggunakan sistem tipe C ++ dengan cara yang sering dipandang sebagai penemuan dan dapat membangkitkan Perilaku Tidak Terdefinisi.

volatilesecara khusus dimaksudkan untuk digunakan ketika berinteraksi dengan perangkat keras yang dipetakan memori, penangan sinyal dan instruksi kode mesin setjmp. Ini membuat volatilelangsung berlaku untuk pemrograman tingkat sistem daripada pemrograman tingkat aplikasi normal.

Standar C ++ 2003 tidak mengatakan bahwa volatileberlaku jenis Acquire atau Release semantik pada variabel. Faktanya, Standar ini sepenuhnya diam mengenai semua hal multithreading. Namun, platform tertentu memang menerapkan Acquire dan Release semantik pada volatilevariabel.

[Pembaruan untuk C ++ 11]

C ++ 11 Standard sekarang tidak mengakui multithreading langsung dalam model memori dan lanuage, dan menyediakan fasilitas perpustakaan untuk menghadapinya dengan cara platform independen. Namun semantik volatilemasih belum berubah. volatilemasih bukan mekanisme sinkronisasi. Bjarne Stroustrup mengatakan banyak hal di TCPPPL4E:

Jangan gunakan volatilekecuali dalam kode level rendah yang berhubungan langsung dengan perangkat keras.

Jangan menganggap volatilememiliki makna khusus dalam model memori. Itu tidak. Ini bukan - seperti dalam beberapa bahasa kemudian - mekanisme sinkronisasi. Untuk mendapatkan sinkronisasi, gunakan atomic, a mutex, atau a condition_variable.

[/ Akhiri pembaruan]

Semua di atas berlaku bahasa C ++ itu sendiri, sebagaimana didefinisikan oleh Standar 2003 (dan sekarang Standar 2011). Namun beberapa platform spesifik menambahkan fungsionalitas atau batasan tambahan untuk apa yang volatiletidak. Misalnya, dalam MSVC 2010 (setidaknya) Acquire dan Rilis semantik yang berlaku untuk operasi tertentu pada volatilevariabel. Dari MSDN :

Saat mengoptimalkan, kompiler harus mempertahankan urutan di antara referensi ke objek volatil serta referensi ke objek global lainnya. Khususnya,

Tulisan ke objek yang mudah menguap (volatile write) memiliki semantik rilis; referensi ke objek global atau statis yang terjadi sebelum penulisan ke objek volatil dalam urutan instruksi akan terjadi sebelum penulisan volatil dalam biner yang dikompilasi.

Pembacaan objek volatil (volatile read) memiliki Acquire semantic; referensi ke objek global atau statis yang terjadi setelah pembacaan memori volatile dalam urutan instruksi akan terjadi setelah pembacaan volatile dalam biner yang dikompilasi.

Namun, Anda mungkin mencatat fakta bahwa jika Anda mengikuti tautan di atas, ada beberapa perdebatan di komentar mengenai apakah memperoleh / melepaskan semantik benar-benar berlaku dalam kasus ini.

John Dibling
sumber
19
Sebagian dari saya ingin mengundurkan diri karena nada merendahkan dari jawaban dan komentar pertama. "volatile is nothing" mirip dengan "alokasi memori manual tidak berguna". Jika Anda dapat menulis program multithreaded tanpa volatileitu karena Anda berdiri di pundak orang yang digunakan volatileuntuk mengimplementasikan pustaka threading.
Ben Jackson
20
@Ben hanya karena sesuatu yang menantang keyakinan Anda tidak membuatnya merendahkan
David Heffernan
39
@ Ben: tidak, baca apa yang volatilesebenarnya dilakukan di C ++. Apa yang dikatakan John benar , akhir cerita. Ini tidak ada hubungannya dengan kode aplikasi vs kode perpustakaan, atau "biasa" vs "programmer mahatahu seperti Tuhan" dalam hal ini. volatiletidak perlu dan tidak berguna untuk sinkronisasi antar utas. Perpustakaan pustaka tidak dapat diimplementasikan dalam hal volatile; itu harus bergantung pada detail platform khusus, dan ketika Anda mengandalkan itu, Anda tidak lagi perlu volatile.
Jalf
6
@jalf: "volatile tidak perlu dan tidak berguna untuk sinkronisasi antar utas" (yang Anda katakan) bukanlah hal yang sama dengan "volatile tidak berguna untuk pemrograman multithreaded" (seperti yang dikatakan John dalam jawabannya). Anda 100% benar, tetapi saya tidak setuju dengan John (sebagian) - volatile masih dapat digunakan untuk pemrograman multithreaded (untuk serangkaian tugas yang sangat terbatas)
4
@ GM: Semua yang berguna hanya berguna di bawah sekumpulan persyaratan atau ketentuan tertentu. Volatile berguna untuk pemrograman multithread di bawah serangkaian kondisi yang ketat (dan dalam beberapa kasus, bahkan mungkin lebih baik (untuk beberapa definisi yang lebih baik) daripada alternatif). Anda mengatakan "mengabaikan ini dan .." tetapi kasus ketika volatile berguna untuk multithreading tidak mengabaikan apa pun. Anda membuat sesuatu yang tidak pernah saya klaim. Ya, kegunaan volatile terbatas, tetapi memang ada - tetapi kita semua bisa sepakat bahwa itu TIDAK berguna untuk sinkronisasi.
31

(Catatan Editor: di C ++ 11 volatilebukan alat yang tepat untuk pekerjaan ini dan masih memiliki data-race UB. Gunakan std::atomic<bool>dengan std::memory_order_relaxedbanyak / toko untuk melakukan ini tanpa UB. Pada implementasi nyata itu akan dikompilasi ke asm yang sama seperti volatile. Saya menambahkan jawaban dengan lebih detail, dan juga mengatasi kesalahpahaman dalam komentar bahwa memori yang tertata buruk mungkin menjadi masalah untuk kasus penggunaan ini: semua CPU dunia nyata memiliki memori bersama yang koheren sehingga volatileakan bekerja untuk ini pada implementasi C ++ nyata. dapat melakukannya.

Beberapa diskusi dalam komentar tampaknya berbicara tentang kasus penggunaan lain di mana Anda akan membutuhkan sesuatu yang lebih kuat daripada atom santai. Jawaban ini sudah menunjukkan yang tidak volatilememberi Anda pemesanan.)


Volatile kadang-kadang berguna karena alasan berikut: kode ini:

/* global */ bool flag = false;

while (!flag) {}

dioptimalkan oleh gcc ke:

if (!flag) { while (true) {} }

Yang jelas salah jika bendera ditulis oleh utas lainnya. Perhatikan bahwa tanpa optimasi ini mekanisme sinkronisasi mungkin bekerja (tergantung pada kode lain beberapa hambatan memori mungkin diperlukan) - tidak perlu untuk mutex dalam 1 produsen - 1 skenario konsumen.

Kalau tidak, kata kunci yang mudah menguap terlalu aneh untuk dapat digunakan - kata kunci tersebut tidak memberikan jaminan pemesanan memori yang menggunakan akses yang mudah menguap dan tidak mudah menguap dan tidak menyediakan operasi atom apa pun - yaitu, Anda tidak mendapat bantuan dari kompiler dengan kata kunci yang mudah menguap kecuali caching register yang dinonaktifkan. .

zeuxcg
sumber
4
Jika saya ingat, atom C ++ 0x, dimaksudkan untuk melakukan dengan benar apa yang diyakini banyak orang (salah) dilakukan oleh volatile.
David Heffernan
14
volatiletidak mencegah akses memori diatur ulang. volatileakses tidak akan dipesan ulang sehubungan satu sama lain, tetapi mereka tidak memberikan jaminan tentang pemesanan ulang sehubungan dengan non- volatileobjek, dan karenanya, mereka pada dasarnya tidak berguna sebagai bendera juga.
Jalf
14
@ Ben: Saya pikir Anda sudah terbalik. Kerumunan "volatile is nothing" bergantung pada fakta sederhana bahwa volatile tidak melindungi terhadap penataan ulang , yang berarti ia sama sekali tidak berguna untuk sinkronisasi. Pendekatan lain mungkin sama-sama tidak berguna (seperti yang Anda sebutkan, optimasi kode waktu tautan memungkinkan kompiler untuk mengintip ke dalam kode yang Anda anggap kompiler akan diperlakukan sebagai kotak hitam), tetapi itu tidak memperbaiki kekurangan volatile.
Jalf
15
@jalf: Lihat artikel oleh Arch Robinson (ditautkan di tempat lain di halaman ini), komentar ke 10 (oleh "Spud"). Pada dasarnya, penataan ulang tidak mengubah logika kode. Kode yang diposting menggunakan tanda untuk membatalkan tugas (bukan untuk memberi tanda bahwa tugas telah selesai), jadi tidak masalah jika tugas dibatalkan sebelum atau setelah kode (misalnya:, while (work_left) { do_piece_of_work(); if (cancel) break;}jika pembatalan disusun ulang dalam loop, logikanya masih valid. Saya punya sepotong kode yang bekerja sama: jika utas utama ingin mengakhiri, ia menetapkan bendera untuk utas lainnya, tetapi tidak ...
15
... masalah jika utas lainnya melakukan beberapa iterasi tambahan dari loop pekerjaan mereka sebelum mereka berakhir, selama itu terjadi secara wajar segera setelah bendera ditetapkan. Tentu saja, ini adalah HANYA penggunaan yang dapat saya pikirkan dan agak ceruk (dan mungkin tidak bekerja pada platform di mana menulis ke variabel yang tidak stabil membuat perubahan terlihat oleh utas lainnya, meskipun pada setidaknya x86 dan x86-64 ini bekerja). Saya tentu tidak akan menyarankan siapa pun untuk benar-benar melakukan itu tanpa alasan yang sangat bagus, saya hanya mengatakan bahwa pernyataan selimut seperti "volatile TIDAK PERNAH berguna dalam kode multithreaded" tidak 100% benar.
16

Dalam C ++ 11, biasanya tidak pernah digunakan volatileuntuk threading, hanya untuk MMIO

Tapi TL: DR, itu "bekerja" seperti atom dengan mo_relaxedpada perangkat keras dengan cache yang koheren (yaitu semuanya); itu cukup untuk menghentikan kompiler menjaga vars di register. atomictidak memerlukan penghalang memori untuk membuat atomicity atau visibilitas antar-thread, hanya untuk membuat utas saat ini menunggu sebelum / setelah operasi untuk membuat pemesanan antara akses utas ini ke variabel yang berbeda. mo_relaxedtidak pernah membutuhkan penghalang, hanya memuat, menyimpan, atau RMW.

Untuk menggulung atom Anda sendiri dengan volatile(dan inline-asm untuk hambatan) di masa lalu yang buruk sebelum C ++ 11 std::atomic, volatileadalah satu-satunya cara yang baik untuk membuat beberapa hal bekerja . Tapi itu tergantung pada banyak asumsi tentang bagaimana implementasi bekerja dan tidak pernah dijamin oleh standar apa pun.

Sebagai contoh, kernel Linux masih menggunakan atom gulungan tangan sendiri volatile, tetapi hanya mendukung beberapa implementasi C tertentu (GNU C, dentang, dan mungkin ICC). Sebagian itu karena ekstensi GNU C dan sintaks dan semantik asm inline, tetapi juga karena itu tergantung pada beberapa asumsi tentang cara kerja kompiler.

Ini hampir selalu merupakan pilihan yang salah untuk proyek baru; Anda dapat menggunakan std::atomic(dengan std::memory_order_relaxed) untuk mendapatkan kompiler untuk memancarkan kode mesin efisien yang sama dengan yang Anda bisa volatile. std::atomicdengan mo_relaxedobsolet volatileuntuk tujuan threading. (Kecuali mungkin untuk mengatasi bug optimisasi yang terlewatkan atomic<double>pada beberapa kompiler .)

Implementasi internal std::atomickompiler arus utama (seperti gcc dan dentang) tidak hanya digunakan secara volatileinternal; kompiler secara langsung memaparkan fungsi muatan atom, penyimpanan, dan fungsi bawaan RMW. (mis. GNU C __atomicbawaan yang beroperasi pada objek "biasa")


Volatile dapat digunakan dalam praktek (tapi jangan lakukan itu)

Yang mengatakan, volatileapakah dapat digunakan dalam praktek untuk hal-hal seperti exit_nowflag pada semua (?) Implementasi C ++ yang ada pada CPU nyata, karena bagaimana CPU bekerja (cache yang koheren) dan berbagi asumsi tentang bagaimana volatileseharusnya bekerja. Tapi tidak banyak lagi, dan tidak direkomendasikan. Tujuan dari jawaban ini adalah untuk menjelaskan bagaimana CPU yang ada dan implementasi C ++ benar-benar bekerja. Jika Anda tidak peduli tentang itu, yang perlu Anda ketahui adalah bahwa std::atomicdengan mo_relaxed obsoletes volatileuntuk threading.

(Standar ISO C ++ cukup samar di atasnya, hanya mengatakan bahwa volatileakses harus dievaluasi secara ketat sesuai dengan aturan mesin abstrak C ++, tidak dioptimalkan. Mengingat bahwa implementasi nyata menggunakan ruang alamat memori mesin untuk memodelkan ruang alamat C ++, ini berarti volatilemembaca dan tugas harus dikompilasi untuk memuat / menyimpan instruksi untuk mengakses representasi objek dalam memori.)


Sebagai jawaban lain menunjukkan, exit_nowbendera adalah kasus sederhana komunikasi antar-thread yang tidak memerlukan sinkronisasi : itu tidak mempublikasikan bahwa isi array siap atau semacamnya. Hanya toko yang segera diperhatikan oleh beban yang tidak dioptimalkan-pergi di utas lain.

    // global
    bool exit_now = false;

    // in one thread
    while (!exit_now) { do_stuff; }

    // in another thread, or signal handler in this thread
    exit_now = true;

Tanpa volatile atau atomik, aturan as-if dan asumsi tidak ada data-ras UB memungkinkan kompiler untuk mengoptimalkannya menjadi asm yang hanya memeriksa bendera sekali , sebelum memasukkan (atau tidak) loop tak terbatas. Inilah yang terjadi dalam kehidupan nyata untuk penyusun nyata. (Dan biasanya mengoptimalkan sebagian besar do_stuffkarena loop tidak pernah keluar, sehingga setiap kode nanti yang mungkin menggunakan hasilnya tidak dapat dijangkau jika kita memasukkan loop).

 // Optimizing compilers transform the loop into asm like this
    if (!exit_now) {        // check once before entering loop
        while(1) do_stuff;  // infinite loop
    }

Program multithreading terjebak dalam mode yang dioptimalkan tetapi berjalan normal di -O0 adalah contoh (dengan deskripsi output asm GCC) tentang bagaimana sebenarnya ini terjadi dengan GCC pada x86-64. Juga pemrograman MCU - optimasi C ++ O2 rusak saat loop pada electronics.SE menunjukkan contoh lain.

Kami biasanya menginginkan optimisasi agresif yang CSE dan hoist memuatkan keluar dari loop, termasuk untuk variabel global.

Sebelum C ++ 11, volatile bool exit_nowadalah salah satu cara untuk membuat pekerjaan ini sebagaimana dimaksud (pada implementasi C ++ normal). Tetapi dalam C ++ 11, perlombaan data UB masih berlaku volatilesehingga sebenarnya tidak dijamin oleh standar ISO untuk bekerja di mana saja, bahkan dengan asumsi cache koheren HW.

Perhatikan bahwa untuk tipe yang lebih luas, volatiletidak memberikan jaminan kurangnya sobek. Saya mengabaikan perbedaan itu di sini boolkarena itu bukan masalah pada implementasi normal. Tapi itu juga bagian dari alasan mengapa volatilemasih tunduk pada perlombaan data UB bukannya setara dengan atom santai.

Perhatikan bahwa "sebagaimana dimaksud" tidak berarti utas melakukan exit_nowmenunggu utas lainnya benar-benar keluar. Atau bahkan menunggu exit_now=truetoko volatil untuk terlihat secara global sebelum melanjutkan operasi selanjutnya di utas ini. ( atomic<bool>dengan default mo_seq_cstakan membuatnya menunggu sebelum seq_cst nanti memuat setidaknya. Pada banyak SPA Anda hanya akan mendapatkan penghalang penuh setelah toko).

C ++ 11 menyediakan cara non-UB yang mengkompilasi yang sama

A "terus berjalan" atau "exit sekarang" bendera harus menggunakan std::atomic<bool> flagdenganmo_relaxed

Menggunakan

  • flag.store(true, std::memory_order_relaxed)
  • while( !flag.load(std::memory_order_relaxed) ) { ... }

akan memberikan Anda asm yang sama persis (tanpa instruksi penghalang mahal) yang akan Anda dapatkan volatile flag.

Selain tanpa robek, atomicjuga memberi Anda kemampuan untuk menyimpan di satu utas dan memuat di utas lainnya tanpa UB, sehingga kompiler tidak dapat mengangkat beban keluar dari satu loop. (Asumsi tidak ada perlombaan data UB adalah yang memungkinkan optimalisasi agresif yang kita inginkan untuk objek non-atom yang tidak mudah menguap.) Fitur atomic<T>ini hampir sama dengan apa yang volatiledilakukan untuk muatan murni dan penyimpanan murni.

atomic<T>juga buat +=dan seterusnya ke dalam operasi atom RMW (secara signifikan lebih mahal daripada beban atom menjadi sementara, operasikan, lalu simpan atom terpisah. Jika Anda tidak menginginkan RMW atom, tulis kode Anda dengan temporer lokal).

Dengan seq_cstpemesanan default yang Anda dapatkan while(!flag), itu juga menambahkan jaminan pemesanan. akses non-atom, dan ke akses atom lainnya.

(Secara teori, standar ISO C ++ tidak mengesampingkan optimasi waktu-kompilasi atom. Tetapi dalam praktiknya kompiler tidak melakukannya karena tidak ada cara untuk mengontrol kapan itu tidak akan terjadi. Ada beberapa kasus di mana bahkan volatile atomic<T>mungkin tidak menjadi cukup kontrol atas optimalisasi atomics jika kompiler melakukannya mengoptimalkan, jadi untuk sekarang compiler tidak. Lihat Mengapa tidak kompiler menggabungkan std berlebihan :: atom menulis? Perhatikan bahwa wg21 / p0062 merekomendasikan untuk tidak menggunakan volatile atomickode saat ini untuk menjaga terhadap optimalisasi atom.)


volatile benar-benar berfungsi untuk ini pada CPU nyata (tapi masih tidak menggunakannya)

bahkan dengan model memori yang tidak tertata dengan baik (non-x86) . Tapi tidak benar-benar menggunakannya, gunakan atomic<T>dengan mo_relaxedsebaliknya !! Inti dari bagian ini adalah untuk mengatasi kesalahpahaman tentang bagaimana CPU bekerja, bukan untuk membenarkan volatile. Jika Anda menulis kode tanpa kunci, Anda mungkin peduli dengan kinerja. Memahami cache dan biaya komunikasi antar thread biasanya penting untuk kinerja yang baik.

CPU nyata memiliki cache yang koheren / memori bersama: setelah penyimpanan dari satu inti menjadi terlihat secara global, tidak ada inti lain yang dapat memuat nilai basi. (Lihat juga Pemrogram Mitos Percaya tentang Cache CPU yang berbicara tentang volatile Java, setara dengan C ++ atomic<T>dengan urutan memori seq_cst.)

Ketika saya mengatakan memuat , maksud saya instruksi asm yang mengakses memori. Itulah yang dijamin oleh volatileakses, dan bukan hal yang sama dengan konversi nilai-ke-nilai dari variabel C ++ non-atom / non-volatil. (misalnya local_tmp = flagatau while(!flag)).

Satu-satunya hal yang perlu Anda kalahkan adalah optimasi waktu kompilasi yang tidak dimuat ulang sama sekali setelah pemeriksaan pertama. Setiap muatan + cek pada setiap iterasi sudah cukup, tanpa pemesanan. Tanpa sinkronisasi antara utas ini dan utas utama, tidak berarti untuk membicarakan kapan tepatnya toko terjadi, atau memesan wrt beban. operasi lain dalam loop. Hanya ketika itu terlihat oleh utas ini yang penting. Ketika Anda melihat flag exit_now diatur, Anda keluar. Latensi antar-inti pada x86 X86 khas dapat berupa sekitar 40ns antara inti fisik yang terpisah .


Secara teori: C ++ utas pada perangkat keras tanpa cache yang koheren

Saya tidak melihat cara ini bisa jauh efisien, hanya dengan ISO C ++ murni tanpa memerlukan programmer untuk melakukan flushes eksplisit dalam kode sumber.

Secara teori Anda bisa memiliki implementasi C ++ pada mesin yang tidak seperti ini, membutuhkan flushes eksplisit yang dihasilkan compiler untuk membuat sesuatu terlihat oleh utas lainnya pada core lainnya . (Atau untuk dibaca agar tidak menggunakan salinan yang mungkin basi). Standar C ++ tidak membuat ini mustahil, tetapi model memori C ++ dirancang agar efisien pada mesin memori bersama yang koheren. Misalnya standar C ++ bahkan berbicara tentang "baca-baca koherensi", "tulis-baca koherensi", dll. Satu catatan dalam standar bahkan menunjukkan koneksi ke perangkat keras:

http://eel.is/c++draft/intro.races#19

[Catatan: Keempat persyaratan koherensi sebelumnya secara efektif melarang penyusunan kembali penyusun kompiler dari operasi atom ke satu objek, bahkan jika kedua operasi tersebut merupakan beban yang santai. Ini secara efektif membuat jaminan koherensi cache yang disediakan oleh sebagian besar perangkat keras tersedia untuk operasi atom C ++. - catatan akhir]

Tidak ada mekanisme bagi releasetoko untuk hanya menyiram dirinya sendiri dan beberapa rentang alamat tertentu: ia harus menyinkronkan semuanya karena tidak akan tahu apa utas lain yang mungkin ingin dibaca jika mereka memperoleh beban melihat toko rilis ini (membentuk sebuah rilis-urutan yang menetapkan hubungan sebelum-terjadi di seluruh utas, menjamin bahwa operasi non-atom sebelumnya yang dilakukan oleh utas penulisan sekarang aman untuk dibaca. Kecuali jika itu menulis lebih lanjut kepada mereka setelah toko rilis ...) Atau kompiler akan memiliki menjadi sangat pintar untuk membuktikan bahwa hanya beberapa baris cache yang diperlukan pembilasan.

Terkait: jawaban saya pada apakah mov + mfence aman di NUMA? masuk ke detail tentang tidak adanya sistem x86 tanpa memori bersama yang koheren. Terkait juga: Memuat dan menyimpan pemesanan ulang pada ARM untuk lebih lanjut tentang memuat / menyimpan ke lokasi yang sama .

Ada yang saya pikir cluster dengan non-koheren memori bersama, tapi mereka tidak mesin-sistem-gambar tunggal. Setiap domain koherensi menjalankan kernel terpisah, jadi Anda tidak dapat menjalankan utas program C ++ tunggal di atasnya. Alih-alih Anda menjalankan program yang terpisah dari program (masing-masing dengan ruang alamat mereka sendiri: pointer dalam satu contoh tidak valid di yang lain).

Untuk membuat mereka berkomunikasi satu sama lain melalui flushes eksplisit, Anda biasanya akan menggunakan MPI atau API lewat pesan untuk membuat program menentukan rentang alamat mana yang perlu dibilas.


Perangkat keras nyata tidak berjalan std::threadmelintasi batas koherensi cache:

Beberapa chip ARM asimetris ada, dengan ruang alamat fisik bersama tetapi tidak domain cache yang dapat dibagikan dalam. Jadi tidak koheren. (mis. utas komentar inti A8 dan Cortex-M3 seperti TI Sitara AM335x).

Tetapi kernel yang berbeda akan berjalan pada core tersebut, bukan gambar sistem tunggal yang dapat menjalankan thread di kedua core. Saya tidak mengetahui adanya implementasi C ++ yang menjalankan std::threadutas lintas inti CPU tanpa cache yang koheren.

Khusus untuk ARM, GCC dan dentang menghasilkan kode dengan asumsi semua utas berjalan dalam domain yang dapat dibagikan dalam-dalam yang sama. Bahkan, kata manual ARMv7 ISA

Arsitektur ini (ARMv7) ditulis dengan harapan bahwa semua prosesor yang menggunakan sistem operasi atau hypervisor yang sama berada dalam domain yang dapat dibagikan dalam yang sama.

Jadi memori bersama yang tidak koheren antara domain yang terpisah hanya merupakan hal untuk penggunaan khusus sistem yang eksplisit dari wilayah memori bersama untuk komunikasi antara berbagai proses di bawah kernel yang berbeda.

Lihat juga diskusi CoreCLR ini tentang kode-gen yang menggunakan dmb ishpenghalang dmb symemori (Batin Shareable) vs. (Sistem) hambatan memori dalam kompiler itu.

Saya membuat pernyataan bahwa tidak ada implementasi C ++ untuk ISA lainnya yang berjalan std::threadlintas core dengan cache yang tidak koheren. Saya tidak punya bukti bahwa tidak ada implementasi seperti itu, tetapi tampaknya sangat tidak mungkin. Kecuali jika Anda menargetkan potongan HW eksotis tertentu yang berfungsi seperti itu, pemikiran Anda tentang kinerja harus mengasumsikan koherensi cache mirip MESI antara semua utas. ( atomic<T>Meskipun demikian, lebih disukai digunakan dengan cara yang menjamin kebenaran!)


Cache yang koheren membuatnya mudah

Tetapi pada sistem multi-core dengan cache yang koheren, mengimplementasikan rilis-store berarti memesan komit ke cache untuk toko thread ini, tidak melakukan pembilasan eksplisit. ( https://preshing.com/20120913/acquire-and-release-semantics/ dan https://preshing.com/20120710/memory-barriers-are-like-source-control-operations/ ). (Dan mendapatkan-memuat berarti memesan akses ke cache di inti lainnya).

Instruksi penghalang memori hanya memblokir beban dan / atau penyimpanan thread saat ini hingga buffer penyimpanan habis; itu selalu terjadi secepat mungkin sendiri. ( Apakah penghalang memori memastikan bahwa koherensi cache telah selesai? Alamat kesalahpahaman ini). Jadi jika Anda tidak perlu memesan, cukup tampilkan visibilitas di utas lain, mo_relaxedtidak masalah. (Begitu juga volatile, tapi jangan lakukan itu.)

Lihat juga pemetaan C / C ++ 11 ke prosesor

Fakta menyenangkan: pada x86, setiap toko asm adalah toko rilis karena model memori x86 pada dasarnya adalah seq-cst plus buffer toko (dengan penerusan toko).


Re terkait semi: penyangga toko, visibilitas global, dan koherensi: C ++ 11 menjamin sangat sedikit. Kebanyakan ISA sebenarnya (kecuali PowerPC) menjamin bahwa semua utas dapat menyetujui urutan penampilan dua toko oleh dua utas lainnya. (Dalam terminologi model memori arsitektur-komputer formal, mereka "multi-copy atomic").

Kesalahpahaman lain adalah bahwa instruksi memori pagar diperlukan untuk menyiram buffer toko untuk core lain untuk melihat toko kami sama sekali . Sebenarnya buffer toko selalu berusaha untuk mengeringkan dirinya sendiri (komit ke cache L1d) secepat mungkin, jika tidak maka akan mengisi dan menghentikan eksekusi. Apa yang dilakukan penghalang / pagar penuh adalah menunda utas saat ini sampai buffer toko dikeringkan , sehingga muatan kami yang kemudian muncul dalam urutan global setelah toko sebelumnya.

(Model memori asm x86 yang tertata sangat berarti bahwa volatilepada x86 mungkin berakhir memberi Anda lebih dekat mo_acq_rel, kecuali bahwa penyusunan ulang waktu kompilasi dengan variabel non-atom masih dapat terjadi. Tetapi sebagian besar non-x86 memiliki model memori yang dipesan dengan lemah sehingga volatiledan relaxedhampir sama lemah karena mo_relaxedmemungkinkan.)

Peter Cordes
sumber
Komentar bukan untuk diskusi panjang; percakapan ini telah dipindahkan ke obrolan .
Samuel Liew
2
Tulisan yang bagus. Ini persis apa yang saya cari (memberikan semua fakta) daripada pernyataan selimut yang hanya mengatakan "menggunakan atom bukan volatil untuk satu bendera boolean bersama global".
bernie
2
@bernie: Saya menulis ini setelah merasa frustrasi dengan klaim berulang yang tidak menggunakan atomicdapat menyebabkan utas berbeda memiliki nilai berbeda untuk variabel yang sama dalam cache . /Telapak tangan. Dalam cache, tidak, di register CPU ya (dengan variabel non-atom); CPU menggunakan cache yang koheren. Saya berharap pertanyaan lain tentang SO tidak penuh dengan penjelasan untuk atomicpenyebaran kesalahpahaman tentang cara kerja CPU. (Karena itu adalah hal yang berguna untuk dipahami karena alasan kinerja, dan juga membantu menjelaskan mengapa aturan atom ISO C ++ ditulis sebagaimana adanya.)
Peter Cordes
-1
#include <iostream>
#include <thread>
#include <unistd.h>
using namespace std;

bool checkValue = false;

int main()
{
    std::thread writer([&](){
            sleep(2);
            checkValue = true;
            std::cout << "Value of checkValue set to " << checkValue << std::endl;
        });

    std::thread reader([&](){
            while(!checkValue);
        });

    writer.join();
    reader.join();
}

Suatu kali seorang pewawancara yang juga percaya bahwa volatile tidak berguna berdebat dengan saya bahwa Optimasi tidak akan menyebabkan masalah dan merujuk ke core yang berbeda memiliki garis cache yang terpisah dan semua itu (tidak benar-benar mengerti apa yang ia maksudkan dengan tepat). Tetapi potongan kode ini ketika dikompilasi dengan -O3 pada g ++ (g ++ -O3 thread.cpp -lpthread), ini menunjukkan perilaku yang tidak terdefinisi. Pada dasarnya jika nilai ditetapkan sebelum memeriksa sementara itu berfungsi dengan baik dan jika tidak itu menjadi loop tanpa repot-repot untuk mengambil nilai (yang sebenarnya diubah oleh utas lainnya). Pada dasarnya saya percaya nilai checkValue hanya akan diambil sekali ke dalam register dan tidak pernah diperiksa lagi di bawah tingkat optimasi tertinggi. Jika disetel ke true sebelum pengambilan, itu berfungsi dengan baik dan jika tidak itu akan menjadi loop. Harap perbaiki saya jika saya salah.

Anu Siril
sumber
4
Apa hubungannya ini volatile? Ya, kode ini adalah UB - tetapi juga UB volatile.
David Schwartz
-2

Anda perlu stabil dan mungkin mengunci.

volatile memberi tahu pengoptimal bahwa nilainya dapat berubah secara tidak sinkron

volatile bool flag = false;

while (!flag) {
    /*do something*/
}

akan membaca flag setiap kali di loop.

Jika Anda mematikan pengoptimalan atau membuat setiap variabel tidak stabil, program akan berperilaku sama tetapi lebih lambat. volatile hanya berarti 'Saya tahu Anda mungkin baru saja membacanya dan tahu apa yang dikatakannya, tetapi jika saya mengatakan membacanya maka bacalah.

Mengunci adalah bagian dari program. Jadi, omong-omong, jika Anda menerapkan semafor maka di antara hal-hal lain itu pasti tidak stabil. (Jangan mencobanya, itu sulit, mungkin akan membutuhkan assembler kecil atau barang-barang atom baru, dan itu sudah dilakukan.)

ctrl-alt-delor
sumber
1
Tetapi bukankah ini, dan contoh yang sama dalam respons yang lain, sibuk menunggu dan dengan demikian sesuatu yang harus dihindari? Jika ini adalah contoh yang dibuat-buat, apakah ada contoh kehidupan nyata yang tidak dibuat-buat?
David Preston
7
@ Chris: Menunggu yang sibuk terkadang merupakan solusi yang baik. Secara khusus, jika Anda berharap hanya perlu menunggu beberapa siklus clock, itu membawa jauh lebih sedikit overhead daripada pendekatan yang jauh lebih berat dari menangguhkan utas. Tentu saja, seperti yang saya sebutkan di komentar lain, contoh seperti ini cacat karena mereka menganggap membaca / menulis ke bendera tidak akan dipesan ulang sehubungan dengan kode yang dilindungi, dan tidak ada jaminan seperti itu diberikan, dan sebagainya , volatiletidak terlalu berguna bahkan dalam kasus ini. Tapi menunggu yang sibuk adalah teknik yang kadang-kadang berguna.
Jalf
3
@ Richard Ya dan tidak. Paruh pertama benar. Tetapi ini hanya berarti bahwa CPU dan kompiler tidak diperbolehkan untuk menyusun ulang variabel volatil sehubungan satu sama lain. Jika saya membaca variabel volatil A, dan kemudian membaca variabel volatil B, maka kompiler harus memancarkan kode yang dijamin (bahkan dengan CPU reordering) untuk membaca A sebelum B. Tetapi itu tidak membuat jaminan tentang semua akses variabel non-volatil . Mereka dapat disusun ulang di sekitar volatile baca / tulis Anda dengan baik. Jadi, kecuali jika Anda membuat setiap variabel dalam program Anda tidak stabil, itu tidak akan memberi Anda jaminan yang Anda minati
jalf
2
@ ctrl-alt-delor: Bukan itu maksudnya volatile"no reordering". Anda berharap itu berarti bahwa toko akan terlihat secara global (ke utas lainnya) dalam urutan program. Itulah yang terjadi atomic<T>dengan memory_order_releaseatau seq_cstmemberi Anda. Tetapi volatile hanya memberi Anda jaminan tidak ada penyusunan ulang waktu kompilasi : setiap akses akan muncul dalam asm dalam urutan program. Berguna untuk driver perangkat. Dan bermanfaat untuk interaksi dengan interrupt handler, debugger, atau signal handler pada core / thread saat ini, tetapi tidak untuk berinteraksi dengan core lainnya.
Peter Cordes
1
volatiledalam praktiknya cukup untuk memeriksa keep_runningflag seperti yang Anda lakukan di sini: CPU nyata selalu memiliki cache yang koheren yang tidak memerlukan pembilasan manual. Tapi tidak ada alasan untuk merekomendasikan volatilelebih atomic<T>dengan mo_relaxed; Anda akan mendapatkan asm yang sama.
Peter Cordes