Mengapa volatile tidak dianggap berguna dalam pemrograman multithread C atau C ++?

165

Seperti ditunjukkan dalam jawaban ini yang baru-baru ini saya posting, saya tampaknya bingung tentang utilitas (atau ketiadaan) volatiledalam 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 foodibaca 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 fooharus 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?

Michael Ekstrand
sumber
7
Semua volatile tidak mengatakan bahwa kompiler seharusnya tidak men-cache akses ke variabel volatile. Ia mengatakan apa-apa tentang serialisasi akses tersebut. Ini telah dibahas di sini, saya tidak tahu berapa kali, dan saya pikir pertanyaan ini tidak akan menambah apa pun pada diskusi itu.
4
Dan lagi-lagi, sebuah pertanyaan yang tidak layak untuk itu, dan telah ditanyakan di sini berkali-kali sebelum diangkat. Tolong berhenti melakukan ini.
14
@neil Saya mencari pertanyaan lain, dan menemukan satu, tetapi penjelasan apa pun yang saya lihat entah bagaimana tidak memicu apa yang saya butuhkan untuk benar-benar mengerti mengapa saya salah. Pertanyaan ini telah menghasilkan jawaban seperti itu.
Michael Ekstrand
1
Untuk penelitian mendalam yang mendalam tentang apa yang CPU lakukan dengan data (melalui cache) periksa: rdrop.com/users/paulmck/scalability/paper/whymb.2010.06.07c.pdf
Sassafras_wot
1
@curiousguy Itulah yang saya maksud dengan "bukan kasus di C", di mana ia dapat digunakan untuk menulis ke register perangkat keras dll, dan tidak digunakan untuk multithreading seperti yang biasa digunakan di Jawa.
Monstieur

Jawaban:

213

Masalahnya volatiledalam 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 mengandalkannya volatile sendirian .

Namun, primitif yang harus kita gunakan untuk properti yang tersisa juga menyediakan properti yang volatileberfungsi, sehingga secara efektif tidak perlu.

Untuk akses aman ke data yang dibagikan, kami membutuhkan jaminan bahwa:

  • baca / tulis benar-benar terjadi (bahwa kompiler tidak akan hanya menyimpan nilai dalam register dan menunda memperbarui memori utama sampai nanti)
  • bahwa tidak ada pemesanan ulang terjadi. Asumsikan bahwa kita menggunakan volatilevariabel 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 ?

volatiletidak menjamin poin pertama. Ini juga menjamin bahwa tidak ada pemesanan ulang terjadi antara berbagai membaca / menulis volatile . Semua volatileakses memori akan terjadi dalam urutan yang ditentukan. Itu yang kita butuhkan untuk apa volatileyang dimaksudkan untuk: memanipulasi register I / O atau perangkat keras yang dipetakan memori, tetapi itu tidak membantu kita dalam kode multithreaded di mana volatileobjek sering hanya digunakan untuk menyinkronkan akses ke data yang tidak mudah menguap. Akses-akses itu masih dapat diatur ulang relatif terhadap volatileyang.

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 volatiletidak perlu. Kami hanya dapat menghapus volatilekualifikasi sepenuhnya.

Sejak C ++ 11, variabel atom ( std::atomic<T>) memberi kita semua jaminan yang relevan.

jalf
sumber
5
@ jbcreix: "Itu" yang Anda tanyakan? Hambatan volatile atau memori? Bagaimanapun, jawabannya hampir sama. Mereka berdua harus bekerja baik di tingkat kompiler dan CPU, karena mereka menggambarkan perilaku yang dapat diamati dari program --- jadi mereka harus memastikan bahwa CPU tidak menyusun ulang semuanya, mengubah perilaku yang mereka jamin. Tetapi saat ini Anda tidak dapat menulis sinkronisasi utas portabel, karena hambatan memori bukan bagian dari standar C ++ (sehingga tidak portabel), dan volatiletidak cukup kuat untuk berguna.
Jalf
4
Contoh MSDN melakukan ini, dan mengklaim bahwa instruksi tidak dapat disusun ulang melewati akses yang mudah berubah: msdn.microsoft.com/en-us/library/12a04hfd(v=vs.80).aspx
OJW
27
@ OJW: Tapi kompiler Microsoft mendefinisikan kembali volatilemenjadi penghalang memori penuh (mencegah pemesanan ulang). Itu bukan bagian dari standar, jadi Anda tidak bisa mengandalkan perilaku ini dalam kode portabel.
Jalf
4
@Skizz: tidak, itu di mana "compiler ajaib" bagian dari persamaan datang dalam penghalang memori A harus dipahami oleh. Baik CPU dan compiler. Jika kompiler memahami semantik dari penghalang memori, ia tahu untuk menghindari trik seperti itu (serta menyusun ulang membaca / menulis melintasi penghalang). Dan untungnya, kompiler tidak memahami semantik dari penghalang memori, jadi pada akhirnya, semuanya berhasil. :)
jalf
13
@ Skizz: Utas sendiri selalu merupakan ekstensi yang tergantung platform sebelum C ++ 11 dan C11. Sepengetahuan saya, setiap lingkungan C dan C ++ yang menyediakan ekstensi threading juga menyediakan ekstensi "penghalang memori". Apapun, volatileselalu tidak berguna untuk pemrograman multi-threaded. (Kecuali di bawah Visual Studio, di mana volatile adalah ekstensi penghalang memori.)
Nemo
49

Anda juga dapat mempertimbangkan ini dari Dokumentasi Kernel Linux .

Pemrogram C sering mengambil volatile yang berarti bahwa variabel dapat diubah di luar utas eksekusi saat ini; akibatnya, mereka kadang-kadang tergoda untuk menggunakannya dalam kode kernel ketika struktur data bersama digunakan. Dengan kata lain, mereka telah dikenal untuk memperlakukan tipe volatil sebagai semacam variabel atom mudah, yang bukan mereka. Penggunaan volatile dalam kode kernel hampir tidak pernah benar; dokumen ini menjelaskan alasannya.

Poin kunci untuk memahami sehubungan dengan volatile adalah bahwa tujuannya adalah untuk menekan optimasi, yang hampir tidak pernah benar-benar ingin dilakukan. Dalam kernel, seseorang harus melindungi struktur data bersama dari akses bersamaan yang tidak diinginkan, yang merupakan tugas yang sangat berbeda. Proses melindungi terhadap konkurensi yang tidak diinginkan juga akan menghindari hampir semua masalah terkait optimasi dengan cara yang lebih efisien.

Seperti volatile, kernel primitif yang membuat akses bersamaan ke data yang aman (spinlocks, mutexes, hambatan memori, dll.) Dirancang untuk mencegah optimasi yang tidak diinginkan. Jika mereka digunakan dengan benar, tidak perlu menggunakan volatile juga. Jika volatile masih diperlukan, hampir pasti ada bug dalam kode di suatu tempat. Dalam kode kernel yang ditulis dengan benar, volatile hanya dapat berfungsi untuk memperlambat segalanya.

Pertimbangkan blok khas dari kode kernel:

spin_lock(&the_lock);
do_something_on(&shared_data);
do_something_else_with(&shared_data);
spin_unlock(&the_lock);

Jika semua kode mengikuti aturan penguncian, nilai shared_data tidak dapat berubah secara tak terduga saat the_lock dipegang. Kode lain yang mungkin ingin dimainkan dengan data itu akan menunggu di kunci. Primitif spinlock bertindak sebagai penghalang memori - mereka secara eksplisit ditulis untuk melakukannya - yang berarti bahwa akses data tidak akan dioptimalkan melintasi mereka. Jadi kompiler mungkin berpikir ia tahu apa yang akan ada di shared_data, tetapi panggilan spin_lock (), karena bertindak sebagai penghalang memori, akan memaksanya untuk melupakan apa pun yang ia ketahui. Tidak akan ada masalah optimasi dengan akses ke data itu.

Jika shared_data dinyatakan tidak stabil, penguncian akan tetap diperlukan. Tetapi kompiler juga akan dicegah mengoptimalkan akses ke shared_data dalam bagian kritis, ketika kita tahu bahwa tidak ada orang lain yang dapat bekerja dengannya. Saat kunci ditahan, shared_data tidak mudah menguap. Saat berhadapan dengan data bersama, penguncian yang tepat membuat volatil tidak perlu - dan berpotensi berbahaya.

Kelas penyimpanan yang mudah menguap awalnya dimaksudkan untuk register I / O yang dipetakan oleh memori. Di dalam kernel, register akses juga harus dilindungi oleh kunci, tetapi seseorang juga tidak ingin kompiler "mengoptimalkan" register mengakses dalam bagian kritis. Tetapi, di dalam kernel, akses memori I / O selalu dilakukan melalui fungsi accessor; mengakses memori I / O secara langsung melalui pointer tidak disukai dan tidak berfungsi pada semua arsitektur. Aksesor tersebut ditulis untuk mencegah optimasi yang tidak diinginkan, jadi, sekali lagi, volatile tidak diperlukan.

Situasi lain di mana orang mungkin tergoda untuk menggunakan volatile adalah ketika prosesor sedang sibuk menunggu nilai variabel. Cara yang tepat untuk melakukan penantian yang sibuk adalah:

while (my_variable != what_i_want)
    cpu_relax();

Panggilan cpu_relax () dapat menurunkan konsumsi daya CPU atau menghasilkan prosesor kembar yang mengalami hipertensi; kebetulan juga berfungsi sebagai penghalang memori, jadi, sekali lagi, volatile tidak perlu. Tentu saja, penantian yang sibuk biasanya merupakan tindakan anti-sosial untuk memulai.

Masih ada beberapa situasi langka di mana volatile masuk akal di kernel:

  • Fungsi-fungsi accessor yang disebutkan di atas mungkin menggunakan volatile pada arsitektur di mana akses memori I / O langsung berfungsi. Pada dasarnya, setiap panggilan accessor menjadi bagian penting sendiri dan memastikan bahwa akses terjadi seperti yang diharapkan oleh programmer.

  • Kode perakitan sebaris yang mengubah memori, tetapi yang tidak memiliki efek samping lain yang terlihat, berisiko dihapus oleh GCC. Menambahkan kata kunci yang mudah menguap ke pernyataan asm akan mencegah penghapusan ini.

  • Variabel jiffies spesial karena dapat memiliki nilai yang berbeda setiap kali direferensikan, tetapi dapat dibaca tanpa penguncian khusus. Jadi jiffies bisa berubah-ubah, tetapi penambahan variabel lain dari tipe ini sangat disukai. Jiffies dianggap sebagai masalah "warisan bodoh" (kata-kata Linus) dalam hal ini; memperbaiki itu akan lebih banyak masalah daripada nilainya.

  • Pointer ke struktur data dalam memori yang koheren yang dapat dimodifikasi oleh perangkat I / O, kadang-kadang, dapat berubah-ubah. Sebuah buffer cincin yang digunakan oleh adaptor jaringan, di mana adaptor itu mengubah pointer untuk menunjukkan deskriptor mana yang telah diproses, adalah contoh dari jenis situasi ini.

Untuk sebagian besar kode, tidak ada justifikasi di atas untuk volatile yang berlaku. Akibatnya, penggunaan volatile cenderung dilihat sebagai bug dan akan membawa pemeriksaan tambahan ke kode. Pengembang yang tergoda untuk menggunakan volatile harus mengambil langkah mundur dan berpikir tentang apa yang sebenarnya mereka coba capai.

Komunitas
sumber
3
@ curiousguy: Ya. Lihat juga gcc.gnu.org/onlinedocs/gcc-4.0.4/gcc/Extended-Asm.html .
Sebastian Mach
1
Spin_lock () terlihat seperti panggilan fungsi biasa. Apa yang istimewa tentang hal itu bahwa kompiler akan memperlakukannya secara khusus sehingga kode yang dihasilkan akan "melupakan" nilai shared_data yang telah dibaca sebelum spin_lock () dan disimpan dalam register sehingga nilainya harus dibaca lagi di dalam do_something_on () setelah spin_lock ()?
Disinkronkan
1
@underscore_d Maksud saya adalah bahwa saya tidak bisa mengatakan dari nama fungsi spin_lock () bahwa ia melakukan sesuatu yang istimewa. Saya tidak tahu apa isinya. Khususnya, saya tidak tahu apa yang ada dalam implementasi yang mencegah kompiler mengoptimalkan bacaan selanjutnya.
Disinkronkan
1
Syncopated memiliki poin yang bagus. Ini pada dasarnya berarti bahwa pemrogram harus mengetahui implementasi internal dari "fungsi-fungsi khusus" atau paling tidak mendapat informasi tentang perilaku mereka. Ini menimbulkan pertanyaan tambahan, seperti - apakah fungsi-fungsi khusus ini distandarisasi dan dijamin bekerja dengan cara yang sama pada semua arsitektur dan semua kompiler? Apakah ada daftar fungsi yang tersedia atau setidaknya ada konvensi untuk menggunakan komentar kode untuk memberi sinyal kepada pengembang bahwa fungsi tersebut melindungi kode agar tidak "dioptimalkan"?
JustAMartin
1
@Tuntable: Statis pribadi dapat disentuh oleh kode apa pun, melalui pointer. Dan alamatnya diambil. Mungkin analisis dataflow mampu membuktikan bahwa pointer tidak pernah lolos, tetapi itu secara umum merupakan masalah yang sangat sulit, superlinear dalam ukuran program. Jika Anda memiliki cara untuk memastikan bahwa tidak ada alias, maka memindahkan akses melintasi kunci pengunci harus benar-benar baik-baik saja. Tetapi jika tidak ada alias, tidak ada volatilegunanya juga. Dalam semua kasus, perilaku "panggilan ke fungsi yang tubuhnya tidak dapat dilihat" akan benar.
Ben Voigt
11

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.

Jeremy Friesner
sumber
2
Bendera Boolean Anda mungkin tidak aman. Bagaimana Anda menjamin bahwa pekerja menyelesaikan tugasnya, dan bahwa bendera tetap berada dalam cakupan sampai terbaca (jika terbaca)? Itu adalah pekerjaan untuk sinyal. Volatile baik untuk menerapkan spinlocks sederhana jika tidak ada mutex yang terlibat, karena keamanan alias berarti kompilator mengasumsikan mutex_lock(dan setiap fungsi perpustakaan lainnya) dapat mengubah keadaan variabel flag.
Potatoswatter
6
Jelas itu hanya bekerja jika sifat dari rutinitas pekerja adalah sedemikian rupa sehingga dijamin untuk memeriksa boolean secara berkala. Bendera volatile-bool-dijamin untuk tetap dalam lingkup karena urutan thread-shutdown selalu terjadi sebelum objek yang menyimpan volatile-boolean dihancurkan, dan urutan thread-shutdown memanggil pthread_join () setelah pengaturan bool. pthread_join () akan memblokir sampai utas pekerja telah hilang. Sinyal memiliki masalah mereka sendiri, terutama ketika digunakan bersama dengan multithreading.
Jeremy Friesner
2
Utas pekerja tidak dijamin untuk menyelesaikan pekerjaannya sebelum boolean benar - pada kenyataannya, hampir pasti akan ada di tengah unit kerja ketika bool disetel ke true. Tapi itu tidak masalah ketika utas pekerja menyelesaikan unit kerjanya, karena utas utama tidak akan melakukan apa pun kecuali memblokir di dalam pthread_join () sampai utas pekerja keluar, bagaimanapun juga. Jadi urutan shutdown tertata dengan baik - bool volatil (dan data bersama lainnya) tidak akan dibebaskan sampai setelah pthread_join () kembali, dan pthread_join () tidak akan kembali sampai thread pekerja hilang.
Jeremy Friesner
10
@ Jeremy, Anda benar dalam praktiknya tetapi secara teoritis masih bisa pecah. Pada sistem dua inti, satu inti terus-menerus mengeksekusi utas pekerja Anda. Inti lainnya mengatur bool menjadi true. Namun tidak ada jaminan bahwa inti utas pekerja akan pernah melihat perubahan itu, artinya ia tidak akan pernah berhenti meskipun berulang memeriksa bool. Perilaku ini diizinkan oleh model memori c ++ 0x, java, dan c #. Dalam praktiknya ini tidak akan pernah terjadi karena utas sibuk kemungkinan besar memasukkan penghalang memori di suatu tempat, setelah itu akan melihat perubahan pada bool.
deft_code
4
Ambil sistem POSIX, gunakan kebijakan penjadwalan waktu nyata 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 ++ volatiletidak 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.
FooF
7

volatileberguna (walaupun tidak cukup) untuk menerapkan konstruksi dasar dari spinlock mutex, tetapi begitu Anda memilikinya (atau sesuatu yang superior), Anda tidak memerlukan yang lain volatile.

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;seharusnya

pthread_mutex_t flag_guard_mutex; // contains something volatile
bool my_shared_flag;

Ini tidak hanya merangkum "bagian yang sulit," itu pada dasarnya diperlukan: C tidak termasuk operasi atom yang diperlukan untuk menerapkan mutex; hanya volatileperlu membuat jaminan ekstra tentang operasi biasa .

Sekarang Anda memiliki sesuatu seperti ini:

pthread_mutex_lock( &flag_guard_mutex );
my_local_state = my_shared_flag; // critical section
pthread_mutex_unlock( &flag_guard_mutex );

pthread_mutex_lock( &flag_guard_mutex ); // may alter my_shared_flag
my_shared_flag = ! my_shared_flag; // critical section
pthread_mutex_unlock( &flag_guard_mutex );

my_shared_flag tidak perlu mudah berubah, meskipun tidak mudah terbakar, karena

  1. Utas lain memiliki akses ke sana.
  2. Berarti referensi untuk itu harus diambil kapan-kapan (dengan &operator).
    • (Atau referensi dibawa ke struktur yang mengandung)
  3. pthread_mutex_lock adalah fungsi perpustakaan.
  4. Berarti kompiler tidak dapat mengetahui apakah pthread_mutex_lockentah bagaimana memperoleh referensi itu.
  5. Berarti kompiler harus berasumsi bahwa pthread_mutex_lockmemodifikasi bendera bersama !
  6. Jadi variabel harus dimuat ulang dari memori. volatile, meskipun bermakna dalam konteks ini, tidak relevan.
Potatoswatter
sumber
6

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):

int volatile* reg=IO_MAPPED_REGISTER_ADDRESS;
*reg=1; // turn the fuel on
*reg=2; // ignition
*reg=3; // release
int x=*reg; // fire missiles

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):

atomic_t counter;

...
atomic_inc(&counter);

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.

atomic_inc(&counter);
atomic_inc(&counter);

masih dapat dioptimalkan untuk

atomically {
  counter+=2;
}

jika optimizer cukup pintar (itu tidak mengubah semantik kode).

jpalecek
sumber
6

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

zebrabox
sumber
3
+1 - atomicity terjamin adalah bagian lain dari apa yang saya lewatkan. Saya berasumsi bahwa memuat int adalah atom, sehingga volatile mencegah pemesanan ulang memberikan solusi lengkap di sisi baca. Saya pikir itu asumsi yang layak pada sebagian besar arsitektur, tetapi itu bukan jaminan.
Michael Ekstrand
Kapan individu membaca dan menulis ke memori interruptible dan non-atomic? Apakah ada manfaatnya?
batbrat
5

FAQ comp.programming.threads memiliki penjelasan klasik oleh Dave Butenhof:

T56: Mengapa saya tidak perlu mendeklarasikan variabel bersama VOLATILE?

Saya khawatir, bagaimanapun, tentang kasus-kasus di mana kompiler dan perpustakaan thread memenuhi spesifikasi masing-masing. Compiler C yang sesuai dapat secara global mengalokasikan beberapa variabel yang dibagi (nonvolatile) ke register yang disimpan dan dipulihkan saat CPU dilewatkan dari utas ke utas. Setiap utas akan memiliki nilai pribadi sendiri untuk variabel bersama ini, yang bukan yang kita inginkan dari variabel bersama.

Dalam beberapa hal ini benar, jika kompiler cukup mengetahui tentang masing-masing cakupan variabel dan fungsi pthread_cond_wait (atau pthread_mutex_lock). Dalam praktiknya, sebagian besar penyusun tidak akan mencoba untuk menyimpan salinan salinan data global di seluruh panggilan ke fungsi eksternal, karena terlalu sulit untuk mengetahui apakah rutin tersebut entah bagaimana memiliki akses ke alamat data.

Jadi ya, memang benar bahwa kompiler yang benar-benar sesuai (tapi sangat agresif) dengan ANSI C mungkin tidak bekerja dengan banyak utas tanpa volatile. Tetapi seseorang lebih baik memperbaikinya. Karena SISTEM apa pun (yaitu, secara pragmatis, kombinasi kernel, pustaka, dan kompiler C) yang tidak memberikan jaminan koherensi memori POSIX tidak sesuai dengan standar POSIX. Titik. Sistem TIDAK BISA meminta Anda untuk menggunakan volatile pada variabel bersama untuk perilaku yang benar, karena POSIX hanya mensyaratkan bahwa fungsi sinkronisasi POSIX diperlukan.

Jadi, jika program Anda rusak karena Anda tidak menggunakan volatile, itu BUG. Itu mungkin bukan bug di C, atau bug di perpustakaan utas, atau bug di kernel. Tetapi ini adalah bug SISTEM, dan satu atau lebih komponen tersebut harus bekerja untuk memperbaikinya.

Anda tidak ingin menggunakan volatile, karena, pada sistem mana pun yang membuat perbedaan, itu akan jauh lebih mahal daripada variabel nonvolatile yang tepat. (ANSI C memerlukan "titik sekuens" untuk variabel volatil di setiap ekspresi, sedangkan POSIX hanya memerlukannya pada operasi sinkronisasi - aplikasi berurutan yang dikomputasi secara intensif akan melihat aktivitas memori yang lebih banyak menggunakan volatile, dan, bagaimanapun juga, aktivitas memorilah yang benar-benar memperlambat Anda.)

/ --- [Dave Butenhof] ----------------------- [[email protected]] --- \
| Peralatan Digital Corporation 110 Spit Brook Rd ZKO2-3 / Q18 |
| 603.881.2218, FAX 603.881.0120 Nashua NH 03062-2698 |
----------------- [Better Living Through Concurrency] ---------------- /

Mr Butenhof membahas banyak hal yang sama di pos usenet ini :

Penggunaan "volatile" tidak cukup untuk memastikan visibilitas memori yang tepat atau sinkronisasi antara utas. Penggunaan mutex sudah cukup, dan, kecuali dengan menggunakan berbagai alternatif kode mesin non-portabel, (atau implikasi yang lebih halus dari aturan memori POSIX yang jauh lebih sulit untuk diterapkan secara umum, seperti dijelaskan dalam posting saya sebelumnya), sebuah Mutex adalah PERLU.

Oleh karena itu, seperti dijelaskan Bryan, penggunaan volatile tidak menghasilkan apa-apa selain untuk mencegah kompiler membuat optimasi yang berguna dan diinginkan, tidak memberikan bantuan apa pun dalam membuat kode "thread safe". Anda dipersilakan, tentu saja, untuk menyatakan apa pun yang Anda inginkan sebagai "volatile" - itu adalah atribut penyimpanan ANSI C yang legal. Hanya saja, jangan berharap itu untuk menyelesaikan masalah sinkronisasi utas untuk Anda.

Semua itu berlaku untuk C ++.

Tony Delroy
sumber
Tautan rusak; tampaknya tidak lagi menunjuk pada apa yang ingin Anda kutip. Tanpa teks, ini adalah jawaban yang tidak berarti.
jww
3

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.

Zack Yezek
sumber
3

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.

David
sumber
Contoh-contoh lama mengharuskan program diproses oleh kompiler berkualitas yang cocok untuk pemrograman tingkat rendah. Sayangnya, kompiler "modern" telah mengambil fakta bahwa Standar tidak mengharuskan mereka untuk memproses "volatile" dengan cara yang bermanfaat sebagai indikasi bahwa kode yang mengharuskan mereka untuk melakukannya rusak, daripada mengakui bahwa Standar tidak membuat upaya untuk melarang implementasi yang sesuai tetapi berkualitas rendah sehingga tidak berguna, tetapi tidak dengan cara apa pun memaafkan kompiler berkualitas rendah tetapi sesuai yang telah menjadi populer
supercat
Pada kebanyakan platform, akan cukup mudah untuk mengenali apa yang volatileperlu 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 membuat volatilepekerjaan sesuai kebutuhan merusak tujuan memiliki standar.
supercat