Seperti ditunjukkan dalam jawaban ini yang baru-baru ini saya posting, saya tampaknya bingung tentang utilitas (atau ketiadaan) volatile
dalam konteks pemrograman multi-threaded.
Pemahaman saya adalah ini: setiap kali suatu variabel dapat diubah di luar aliran kontrol sepotong kode yang mengaksesnya, variabel itu harus dinyatakan volatile
. Penangan sinyal, register I / O, dan variabel yang dimodifikasi oleh utas lain semuanya merupakan situasi seperti itu.
Jadi, jika Anda memiliki int global foo
, dan foo
dibaca oleh satu utas dan disetel secara atomis oleh utas lainnya (mungkin menggunakan instruksi mesin yang sesuai), utas pembacaan melihat situasi ini dengan cara yang sama seperti melihat variabel yang di-tweak oleh penangan sinyal atau dimodifikasi oleh kondisi perangkat keras eksternal dan karenanya foo
harus dinyatakan volatile
(atau, untuk situasi multithreaded, diakses dengan beban yang dipagari memori, yang mungkin merupakan solusi yang lebih baik).
Bagaimana dan di mana saya salah?
Jawaban:
Masalahnya
volatile
dalam konteks multithreaded adalah bahwa itu tidak memberikan semua jaminan yang kita butuhkan. Memang ada beberapa properti yang kita butuhkan, tetapi tidak semuanya, jadi kita tidak bisa mengandalkannyavolatile
sendirian .Namun, primitif yang harus kita gunakan untuk properti yang tersisa juga menyediakan properti yang
volatile
berfungsi, sehingga secara efektif tidak perlu.Untuk akses aman ke data yang dibagikan, kami membutuhkan jaminan bahwa:
volatile
variabel sebagai panji untuk menunjukkan apakah beberapa data siap dibaca atau tidak. Dalam kode kami, kami cukup mengatur bendera setelah menyiapkan data, jadi semuanya tampak baik-baik saja. Tetapi bagaimana jika instruksi disusun ulang sehingga bendera diatur terlebih dahulu ?volatile
tidak menjamin poin pertama. Ini juga menjamin bahwa tidak ada pemesanan ulang terjadi antara berbagai membaca / menulis volatile . Semuavolatile
akses memori akan terjadi dalam urutan yang ditentukan. Itu yang kita butuhkan untuk apavolatile
yang dimaksudkan untuk: memanipulasi register I / O atau perangkat keras yang dipetakan memori, tetapi itu tidak membantu kita dalam kode multithreaded di manavolatile
objek sering hanya digunakan untuk menyinkronkan akses ke data yang tidak mudah menguap. Akses-akses itu masih dapat diatur ulang relatif terhadapvolatile
yang.Solusi untuk mencegah pemesanan ulang adalah dengan menggunakan penghalang memori , yang menunjukkan baik untuk kompiler dan CPU bahwa tidak ada akses memori yang dapat dipesan ulang di titik ini . Menempatkan penghalang semacam itu di sekitar akses variabel volatil kami memastikan bahwa akses non-volatil sekalipun tidak akan disusun ulang di seluruh variabel volatil, memungkinkan kami untuk menulis kode aman-thread.
Namun, hambatan ingatan juga memastikan bahwa semua pembacaan / penulisan yang tertunda dijalankan ketika penghalang tercapai, sehingga secara efektif memberi kita segala yang kita butuhkan dengan sendirinya, menjadikannya
volatile
tidak perlu. Kami hanya dapat menghapusvolatile
kualifikasi sepenuhnya.Sejak C ++ 11, variabel atom (
std::atomic<T>
) memberi kita semua jaminan yang relevan.sumber
volatile
tidak cukup kuat untuk berguna.volatile
menjadi penghalang memori penuh (mencegah pemesanan ulang). Itu bukan bagian dari standar, jadi Anda tidak bisa mengandalkan perilaku ini dalam kode portabel.volatile
selalu tidak berguna untuk pemrograman multi-threaded. (Kecuali di bawah Visual Studio, di mana volatile adalah ekstensi penghalang memori.)Anda juga dapat mempertimbangkan ini dari Dokumentasi Kernel Linux .
sumber
volatile
gunanya juga. Dalam semua kasus, perilaku "panggilan ke fungsi yang tubuhnya tidak dapat dilihat" akan benar.Saya tidak berpikir Anda salah - volatile diperlukan untuk menjamin bahwa thread A akan melihat nilai berubah, jika nilainya diubah oleh sesuatu selain thread A. Seperti yang saya pahami, volatile pada dasarnya adalah cara untuk memberi tahu kompiler "jangan cache variabel ini dalam register, sebagai gantinya pastikan untuk selalu membaca / menulisnya dari memori RAM pada setiap akses".
Kebingungannya adalah karena volatile tidak cukup untuk mengimplementasikan sejumlah hal. Secara khusus, sistem modern menggunakan berbagai tingkat caching, CPU multi-core modern melakukan beberapa optimasi yang bagus pada saat run-time, dan kompiler modern melakukan beberapa optimasi yang bagus pada waktu kompilasi, dan ini semua dapat menghasilkan berbagai efek samping yang muncul dalam berbagai memesan dari urutan yang Anda harapkan jika Anda hanya melihat kode sumber.
Jadi volatile baik-baik saja, selama Anda ingat bahwa perubahan 'yang diamati' dalam variabel volatile mungkin tidak terjadi pada waktu yang tepat menurut Anda. Secara khusus, jangan mencoba menggunakan variabel volatil sebagai cara untuk menyinkronkan atau memesan operasi di utas, karena itu tidak akan bekerja dengan andal.
Secara pribadi, penggunaan utama saya (hanya?) Untuk flag volatile adalah sebagai boolean "pleaseGoAwayNow". Jika saya memiliki utas pekerja yang terus menerus berputar, saya akan memeriksanya boolean volatile pada setiap iterasi dari loop, dan keluar jika boolean itu benar. Utas utama kemudian dapat dengan aman membersihkan utas pekerja dengan mengatur boolean menjadi true, dan kemudian memanggil pthread_join () untuk menunggu sampai utas pekerja hilang.
sumber
mutex_lock
(dan setiap fungsi perpustakaan lainnya) dapat mengubah keadaan variabel flag.SCHED_FIFO
, prioritas statis yang lebih tinggi daripada proses / utas lainnya dalam sistem, cukup inti, semestinya dimungkinkan. Di Linux Anda dapat menentukan bahwa proses waktu nyata dapat menggunakan 100% waktu CPU. Mereka tidak akan pernah beralih konteks jika tidak ada utas prioritas / proses yang lebih tinggi dan tidak pernah diblokir oleh I / O. Tetapi intinya adalah bahwa C / C ++volatile
tidak dimaksudkan untuk menegakkan semantik berbagi / sinkronisasi data yang tepat. Saya menemukan mencari kasus-kasus khusus untuk membuktikan bahwa kode yang salah mungkin kadang-kadang bisa berfungsi adalah latihan yang tidak berguna.volatile
berguna (walaupun tidak cukup) untuk menerapkan konstruksi dasar dari spinlock mutex, tetapi begitu Anda memilikinya (atau sesuatu yang superior), Anda tidak memerlukan yang lainvolatile
.Cara khas pemrograman multithreaded bukan untuk melindungi setiap variabel bersama di tingkat mesin, melainkan untuk memperkenalkan variabel penjaga yang memandu aliran program. Daripada
volatile bool my_shared_flag;
seharusnyaIni tidak hanya merangkum "bagian yang sulit," itu pada dasarnya diperlukan: C tidak termasuk operasi atom yang diperlukan untuk menerapkan mutex; hanya
volatile
perlu membuat jaminan ekstra tentang operasi biasa .Sekarang Anda memiliki sesuatu seperti ini:
my_shared_flag
tidak perlu mudah berubah, meskipun tidak mudah terbakar, karena&
operator).pthread_mutex_lock
adalah fungsi perpustakaan.pthread_mutex_lock
entah bagaimana memperoleh referensi itu.pthread_mutex_lock
memodifikasi bendera bersama !volatile
, meskipun bermakna dalam konteks ini, tidak relevan.sumber
Pemahaman Anda benar-benar salah.
Properti, yang dimiliki oleh variabel volatil, adalah "membaca dari dan menulis ke variabel ini adalah bagian dari perilaku program yang dapat dipahami". Itu berarti program ini berfungsi (diberikan perangkat keras yang sesuai):
Masalahnya adalah, ini bukan properti yang kita inginkan dari thread-safe apa pun.
Sebagai contoh, penghitung thread-aman akan menjadi adil (kode seperti linux-kernel, tidak tahu setara c ++ 0x):
Ini atom, tanpa penghalang memori. Anda harus menambahkannya jika perlu. Menambahkan volatile mungkin tidak akan membantu, karena itu tidak akan menghubungkan akses ke kode terdekat (misalnya untuk menambahkan elemen ke daftar yang dihitung penghitung). Tentu saja, Anda tidak perlu melihat penghitung bertambah di luar program Anda, dan optimisasi masih diinginkan, misalnya.
masih dapat dioptimalkan untuk
jika optimizer cukup pintar (itu tidak mengubah semantik kode).
sumber
Agar data Anda konsisten di lingkungan bersamaan, Anda perlu dua syarat untuk diterapkan:
1) Atomicity yaitu jika saya membaca atau menulis beberapa data ke memori maka data tersebut akan dibaca / ditulis dalam satu pass dan tidak dapat diinterupsi atau diperdebatkan karena misalnya saklar konteks
2) Konsistensi yaitu urutan operasi baca / tulis harus dilihat sama antara beberapa lingkungan bersamaan - baik itu utas, mesin, dll
volatile tidak cocok dengan yang di atas - atau lebih khusus lagi, standar c atau c ++ tentang bagaimana volatile seharusnya berperilaku tidak termasuk di atas.
Ini bahkan lebih buruk dalam prakteknya karena beberapa kompiler (seperti kompiler intel Itanium) berusaha untuk mengimplementasikan beberapa elemen perilaku akses aman bersamaan (yaitu dengan memastikan pagar memori) namun tidak ada konsistensi di seluruh implementasi kompiler dan apalagi standar tidak memerlukan ini implementasi di tempat pertama.
Menandai sebuah variabel sebagai volatile hanya akan berarti bahwa Anda memaksa nilai untuk dibuang ke dan dari memori setiap kali yang dalam banyak kasus hanya memperlambat kode Anda karena pada dasarnya Anda telah menghancurkan kinerja cache Anda.
c # dan java AFAIK melakukan perbaikan ini dengan membuat volatile mematuhi 1) dan 2) namun hal yang sama tidak dapat dikatakan untuk kompiler c / c ++ jadi pada dasarnya lakukan dengan itu sesuai keinginan Anda.
Untuk beberapa diskusi lebih mendalam (meskipun tidak bias) pada subjek membaca ini
sumber
FAQ comp.programming.threads memiliki penjelasan klasik oleh Dave Butenhof:
Mr Butenhof membahas banyak hal yang sama di pos usenet ini :
Semua itu berlaku untuk C ++.
sumber
Ini semua yang dilakukan oleh "volatile": "Hai kompiler, variabel ini dapat berubah SETIAP SAAT (pada setiap clock tick) bahkan jika TIDAK ADA INSTRUKSI LOKAL yang menanganinya. JANGAN simpan cache nilai ini dalam register."
Hanya itu saja. Ia memberi tahu kompiler bahwa nilaimu, yah, mudah menguap - nilai ini dapat diubah setiap saat oleh logika eksternal (utas lainnya, proses lain, Kernel, dll.). Itu ada kurang lebih semata-mata untuk menekan optimisasi kompiler yang akan secara diam-diam men-cache nilai dalam register yang secara inheren tidak aman untuk cache EVER.
Anda dapat menemukan artikel seperti "Dr. Dobbs" yang berubah-ubah karena beberapa obat mujarab untuk pemrograman multi-utas. Pendekatannya tidak sepenuhnya tidak berdasar, tetapi memiliki kelemahan mendasar dalam membuat pengguna objek bertanggung jawab atas keamanan utasnya, yang cenderung memiliki masalah yang sama dengan pelanggaran enkapsulasi lainnya.
sumber
Menurut standar C lama saya, "Apa yang merupakan akses ke objek yang memiliki tipe yang mudah menguap adalah implementasi-didefinisikan" . Jadi penulis kompiler C dapat memilih untuk memiliki "volatile" berarti "akses aman thread dalam lingkungan multi-proses" . Tetapi mereka tidak melakukannya.
Sebagai gantinya, operasi yang diperlukan untuk membuat utas bagian kritis aman di lingkungan memori bersama multi-inti multi-proses ditambahkan sebagai fitur yang ditentukan-implementasi baru. Dan, terbebas dari persyaratan bahwa "volatile" akan menyediakan akses atom dan pemesanan akses dalam lingkungan multi-proses, penulis kompiler memprioritaskan pengurangan kode dibandingkan dengan semantik "volatile" yang bergantung pada implementasi historis.
Ini berarti bahwa hal-hal seperti "volatile" semaphore di sekitar bagian kode kritis, yang tidak bekerja pada perangkat keras baru dengan kompiler baru, mungkin pernah bekerja dengan kompiler lama pada perangkat keras lama, dan contoh lama terkadang tidak salah, hanya lama.
sumber
volatile
perlu dilakukan untuk memungkinkan seseorang untuk menulis OS dengan cara yang tergantung pada perangkat keras tetapi tidak bergantung pada kompiler. Mengharuskan pemrogram menggunakan fitur yang tergantung pada implementasi alih-alih membuatvolatile
pekerjaan sesuai kebutuhan merusak tujuan memiliki standar.