Adakah yang bisa memberikan penjelasan yang baik tentang kata kunci yang mudah menguap di C #? Masalah apa yang dipecahkan dan yang tidak? Dalam kasus apa akan menghemat penggunaan penguncian?
c#
multithreading
Doron Yaacoby
sumber
sumber
Jawaban:
Saya tidak berpikir ada orang yang lebih baik untuk menjawab ini daripada Eric Lippert (penekanan pada aslinya):
Untuk bacaan lebih lanjut, lihat:
sumber
volatile
akan ada berdasarkan kunciJika Anda ingin mendapatkan sedikit lebih teknis tentang apa kata kunci volatil, pertimbangkan program berikut (Saya menggunakan DevStudio 2005):
Menggunakan pengaturan kompilator (rilis) standar yang dioptimalkan, kompiler membuat assembler berikut (IA32):
Melihat output, kompiler telah memutuskan untuk menggunakan register ecx untuk menyimpan nilai dari variabel j. Untuk loop non-volatile (yang pertama) kompiler telah menetapkan i ke register eax. Cukup mudah. Ada beberapa bit yang menarik - lea ebx, [ebx] instruksi secara efektif adalah instruksi nib multibyte sehingga loop melompat ke 16 byte alamat memori yang disejajarkan. Yang lainnya adalah penggunaan edx untuk menambah penghitung loop alih-alih menggunakan instruksi inc eax. Instruksi tambah reg, reg memiliki latensi lebih rendah pada beberapa core IA32 dibandingkan dengan instruksi inc reg, tetapi tidak pernah memiliki latensi lebih tinggi.
Sekarang untuk loop dengan penghitung loop volatile. Penghitung disimpan di [esp] dan kata kunci yang mudah menguap memberi tahu kompiler bahwa nilai harus selalu dibaca dari / ditulis ke memori dan tidak pernah ditugaskan ke register. Kompilator bahkan melangkah lebih jauh dengan tidak melakukan load / increment / store sebagai tiga langkah berbeda (load eax, inc eax, save eax) saat memperbarui nilai penghitung, sebagai gantinya memori diubah secara langsung dalam satu instruksi (sebuah add mem , reg). Cara kode telah dibuat memastikan nilai loop counter selalu up-to-date dalam konteks inti CPU tunggal. Tidak ada operasi pada data yang dapat menyebabkan korupsi atau kehilangan data (karenanya tidak menggunakan load / inc / store karena nilainya dapat berubah selama inc sehingga hilang di store). Karena interupsi hanya dapat dilayani setelah instruksi saat ini selesai,
Setelah Anda memperkenalkan CPU kedua ke sistem, kata kunci yang mudah menguap tidak akan melindungi terhadap data yang diperbarui oleh CPU lain secara bersamaan. Dalam contoh di atas, Anda akan memerlukan data untuk tidak selaras untuk mendapatkan potensi korupsi. Kata kunci yang mudah menguap tidak akan mencegah potensi korupsi jika data tidak dapat ditangani secara atomis, misalnya, jika penghitung putaran bertipe panjang panjang (64 bit) maka akan membutuhkan dua operasi 32 bit untuk memperbarui nilai, di tengah-tengah di mana interupsi dapat terjadi dan mengubah data.
Jadi, kata kunci yang mudah menguap hanya baik untuk data yang disejajarkan yang kurang dari atau sama dengan ukuran register asli sehingga operasi selalu atom.
Kata kunci yang mudah menguap dirancang untuk digunakan dengan operasi IO di mana IO akan terus berubah tetapi memiliki alamat yang konstan, seperti memori yang dipetakan perangkat UART, dan kompiler tidak boleh terus menggunakan kembali nilai pertama yang dibaca dari alamat.
Jika Anda menangani data besar atau memiliki banyak CPU, maka Anda akan memerlukan sistem penguncian tingkat tinggi (OS) untuk menangani akses data dengan benar.
sumber
Jika Anda menggunakan .NET 1.1, kata kunci yang mudah menguap diperlukan saat melakukan penguncian ganda diperiksa. Mengapa? Karena sebelum .NET 2.0, skenario berikut ini dapat menyebabkan utas kedua mengakses objek yang tidak nol, namun tidak sepenuhnya dibangun:
Sebelum. NET 2.0, this.foo dapat ditugaskan contoh baru Foo, sebelum konstruktor selesai berjalan. Dalam hal ini, utas kedua dapat masuk (selama panggilan utas 1 ke konstruktor Foo) dan mengalami yang berikut:
Sebelum. NET 2.0, Anda bisa mendeklarasikan this.foo sebagai volatile untuk mengatasi masalah ini. Sejak .NET 2.0, Anda tidak perlu lagi menggunakan kata kunci yang mudah menguap untuk menyelesaikan penguncian berlabel ganda.
Wikipedia sebenarnya memiliki artikel bagus tentang Penguncian Berganda Ganda, dan secara singkat menyentuh topik ini: http://en.wikipedia.org/wiki/Double-checked_locking
sumber
foo
? Tidakkah penguncian utas 1this.bar
dan karenanya hanya utas 1 yang dapat menginisialisasi foo pada suatu titik waktu? Maksud saya, Anda memeriksa nilainya setelah kunci dilepaskan lagi, kapan pun itu harus memiliki nilai baru dari utas 1.Terkadang, kompiler akan mengoptimalkan bidang dan menggunakan register untuk menyimpannya. Jika utas 1 melakukan penulisan ke bidang dan utas lainnya mengaksesnya, karena pembaruan disimpan dalam register (dan bukan memori), utas ke-2 akan mendapatkan data basi.
Anda dapat memikirkan kata kunci yang mudah menguap dengan mengatakan kepada kompiler "Saya ingin Anda menyimpan nilai ini dalam memori". Ini menjamin bahwa utas ke-2 mengambil nilai terbaru.
sumber
Dari MSDN : Pengubah volatil biasanya digunakan untuk bidang yang diakses oleh banyak utas tanpa menggunakan pernyataan kunci untuk membuat serialisasi akses. Menggunakan pengubah volatil memastikan bahwa satu utas mengambil nilai paling baru yang ditulis oleh utas lain.
sumber
CLR suka untuk mengoptimalkan instruksi, jadi ketika Anda mengakses suatu bidang dalam kode, itu mungkin tidak selalu mengakses nilai bidang saat ini (mungkin dari tumpukan, dll). Menandai bidang sebagai
volatile
memastikan bahwa nilai bidang saat ini diakses oleh instruksi. Ini berguna ketika nilainya dapat dimodifikasi (dalam skenario non-penguncian) oleh utas bersamaan di program Anda atau beberapa kode lain yang berjalan di sistem operasi.Anda jelas kehilangan beberapa optimasi, tetapi itu membuat kode lebih sederhana.
sumber
Saya menemukan artikel ini oleh Joydip Kanjilal sangat membantu!
When you mark an object or a variable as volatile, it becomes a candidate for volatile reads and writes. It should be noted that in C# all memory writes are volatile irrespective of whether you are writing data to a volatile or a non-volatile object. However, the ambiguity happens when you are reading data. When you are reading data that is non-volatile, the executing thread may or may not always get the latest value. If the object is volatile, the thread always gets the most up-to-date value
Saya hanya akan meninggalkannya di sini untuk referensi
sumber
Kompiler terkadang mengubah urutan pernyataan dalam kode untuk mengoptimalkannya. Biasanya ini bukan masalah di lingkungan single-threaded, tetapi mungkin masalah di lingkungan multi-threaded. Lihat contoh berikut:
Jika Anda menjalankan t1 dan t2, Anda akan mengharapkan tidak ada output atau "Nilai: 10" sebagai hasilnya. Bisa jadi kompilator beralih baris di dalam fungsi t1. Jika t2 kemudian dieksekusi, bisa jadi _flag memiliki nilai 5, tetapi _value memiliki 0. Jadi logika yang diharapkan bisa dipatahkan.
Untuk memperbaikinya, Anda dapat menggunakan kata kunci volatil yang dapat Anda terapkan ke bidang. Pernyataan ini menonaktifkan optimisasi kompiler sehingga Anda dapat memaksakan urutan yang benar dalam kode Anda.
Anda harus menggunakan volatile hanya jika Anda benar-benar membutuhkannya, karena ia menonaktifkan optimisasi kompiler tertentu, itu akan merusak kinerja. Ini juga tidak didukung oleh semua bahasa .NET (Visual Basic tidak mendukungnya), sehingga menghambat interoperabilitas bahasa.
sumber
Jadi untuk meringkas semua ini, jawaban yang benar untuk pertanyaan ini adalah: Jika kode Anda berjalan di runtime 2.0 atau lebih baru, kata kunci yang mudah menguap hampir tidak pernah dibutuhkan dan tidak lebih berbahaya daripada baik jika digunakan secara tidak perlu. Yaitu Jangan pernah menggunakannya. TETAPI di versi sebelumnya dari runtime, itu diperlukan untuk mengunci pemeriksaan yang tepat pada bidang statis. Bidang khusus statis yang kelasnya memiliki kode inisialisasi kelas statis.
sumber
beberapa utas dapat mengakses suatu variabel. Pembaruan terbaru ada pada variabel
sumber