Dalam C ++ 11, biasanya tidak pernah digunakan volatile
untuk threading, hanya untuk MMIO
Tapi TL: DR, itu "bekerja" seperti atom dengan mo_relaxed
pada perangkat keras dengan cache yang koheren (yaitu semuanya); itu cukup untuk menghentikan kompiler menjaga vars di register. atomic
tidak 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_relaxed
tidak 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
, volatile
adalah 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::atomic
dengan mo_relaxed
obsolet volatile
untuk tujuan threading. (Kecuali mungkin untuk mengatasi bug optimisasi yang terlewatkan atomic<double>
pada beberapa kompiler .)
Implementasi internal std::atomic
kompiler arus utama (seperti gcc dan dentang) tidak hanya digunakan secara volatile
internal; kompiler secara langsung memaparkan fungsi muatan atom, penyimpanan, dan fungsi bawaan RMW. (mis. GNU C __atomic
bawaan yang beroperasi pada objek "biasa")
Volatile dapat digunakan dalam praktek (tapi jangan lakukan itu)
Yang mengatakan, volatile
apakah dapat digunakan dalam praktek untuk hal-hal seperti exit_now
flag pada semua (?) Implementasi C ++ yang ada pada CPU nyata, karena bagaimana CPU bekerja (cache yang koheren) dan berbagi asumsi tentang bagaimana volatile
seharusnya 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::atomic
dengan mo_relaxed obsoletes volatile
untuk threading.
(Standar ISO C ++ cukup samar di atasnya, hanya mengatakan bahwa volatile
akses 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 volatile
membaca dan tugas harus dikompilasi untuk memuat / menyimpan instruksi untuk mengakses representasi objek dalam memori.)
Sebagai jawaban lain menunjukkan, exit_now
bendera 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_stuff
karena 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_now
adalah salah satu cara untuk membuat pekerjaan ini sebagaimana dimaksud (pada implementasi C ++ normal). Tetapi dalam C ++ 11, perlombaan data UB masih berlaku volatile
sehingga sebenarnya tidak dijamin oleh standar ISO untuk bekerja di mana saja, bahkan dengan asumsi cache koheren HW.
Perhatikan bahwa untuk tipe yang lebih luas, volatile
tidak memberikan jaminan kurangnya sobek. Saya mengabaikan perbedaan itu di sini bool
karena itu bukan masalah pada implementasi normal. Tapi itu juga bagian dari alasan mengapa volatile
masih tunduk pada perlombaan data UB bukannya setara dengan atom santai.
Perhatikan bahwa "sebagaimana dimaksud" tidak berarti utas melakukan exit_now
menunggu utas lainnya benar-benar keluar. Atau bahkan menunggu exit_now=true
toko volatil untuk terlihat secara global sebelum melanjutkan operasi selanjutnya di utas ini. ( atomic<bool>
dengan default mo_seq_cst
akan 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> flag
denganmo_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, atomic
juga 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 volatile
dilakukan 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_cst
pemesanan 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 atomic
kode 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_relaxed
sebaliknya !! 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 volatile
akses, dan bukan hal yang sama dengan konversi nilai-ke-nilai dari variabel C ++ non-atom / non-volatil. (misalnya local_tmp = flag
atau 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 release
toko 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::thread
melintasi 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::thread
utas 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 ish
penghalang dmb sy
memori (Batin Shareable) vs. (Sistem) hambatan memori dalam kompiler itu.
Saya membuat pernyataan bahwa tidak ada implementasi C ++ untuk ISA lainnya yang berjalan std::thread
lintas 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_relaxed
tidak 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 volatile
pada 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 volatile
dan relaxed
hampir sama lemah karena mo_relaxed
memungkinkan.)