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?
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?
Jawaban:
Saya tidak berpikir ada orang yang lebih baik untuk menjawab ini daripada Eric Lippert (penekanan pada aslinya):
Dalam C #, "volatile" berarti tidak hanya "pastikan bahwa kompiler dan jitter tidak melakukan pengurutan ulang kode apa pun atau mendaftarkan optimisasi caching pada variabel ini". Ini juga berarti "beri tahu prosesor untuk melakukan apa pun yang perlu mereka lakukan untuk memastikan bahwa saya membaca nilai terbaru, bahkan jika itu berarti menghentikan prosesor lain dan membuat mereka menyinkronkan memori utama dengan cache mereka".
Sebenarnya, bagian terakhir itu bohong. Semantik sejati dari volatile membaca dan menulis jauh lebih kompleks daripada yang saya uraikan di sini; sebenarnya mereka tidak benar-benar menjamin bahwa setiap prosesor menghentikan apa yang dilakukannya dan memperbarui cache ke / dari memori utama. Sebaliknya, mereka memberikan jaminan yang lebih lemah tentang bagaimana memori mengakses sebelum dan sesudah membaca dan menulis dapat diamati untuk dipesan sehubungan satu sama lain . Operasi tertentu seperti membuat utas baru, memasukkan kunci, atau menggunakan salah satu dari keluarga metode yang saling terkait memperkenalkan jaminan yang lebih kuat tentang pengamatan pemesanan. Jika Anda ingin lebih detail, baca bagian 3.10 dan 10.5.3 dari spesifikasi C # 4.0.
Terus terang, saya tidak menyarankan Anda untuk membuat bidang yang tidak stabil . Bidang yang mudah menguap adalah tanda bahwa Anda melakukan sesuatu yang benar-benar gila: Anda mencoba membaca dan menulis nilai yang sama pada dua utas yang berbeda tanpa meletakkan kunci di tempatnya. Kunci menjamin bahwa memori yang dibaca atau dimodifikasi di dalam kunci diamati konsisten, kunci menjamin bahwa hanya satu utas yang mengakses potongan memori yang diberikan pada satu waktu, dan seterusnya. Jumlah situasi di mana kunci terlalu lambat sangat kecil, dan probabilitas bahwa Anda akan mendapatkan kode yang salah karena Anda tidak mengerti model memori yang tepat sangat besar. Saya tidak berusaha untuk menulis kode kunci-rendah kecuali untuk penggunaan yang paling sepele dari operasi yang saling terkait Saya meninggalkan penggunaan "volatile" kepada para ahli nyata.
Untuk bacaan lebih lanjut, lihat:
volatile
akan ada berdasarkan kunci
Jika Anda ingin mendapatkan sedikit lebih teknis tentang apa kata kunci volatil, pertimbangkan program berikut (Saya menggunakan DevStudio 2005):
#include <iostream>
void main()
{
int j = 0;
for (int i = 0 ; i < 100 ; ++i)
{
j += i;
}
for (volatile int i = 0 ; i < 100 ; ++i)
{
j += i;
}
std::cout << j;
}
Menggunakan pengaturan kompilator (rilis) standar yang dioptimalkan, kompiler membuat assembler berikut (IA32):
void main()
{
00401000 push ecx
int j = 0;
00401001 xor ecx,ecx
for (int i = 0 ; i < 100 ; ++i)
00401003 xor eax,eax
00401005 mov edx,1
0040100A lea ebx,[ebx]
{
j += i;
00401010 add ecx,eax
00401012 add eax,edx
00401014 cmp eax,64h
00401017 jl main+10h (401010h)
}
for (volatile int i = 0 ; i < 100 ; ++i)
00401019 mov dword ptr [esp],0
00401020 mov eax,dword ptr [esp]
00401023 cmp eax,64h
00401026 jge main+3Eh (40103Eh)
00401028 jmp main+30h (401030h)
0040102A lea ebx,[ebx]
{
j += i;
00401030 add ecx,dword ptr [esp]
00401033 add dword ptr [esp],edx
00401036 mov eax,dword ptr [esp]
00401039 cmp eax,64h
0040103C jl main+30h (401030h)
}
std::cout << j;
0040103E push ecx
0040103F mov ecx,dword ptr [__imp_std::cout (40203Ch)]
00401045 call dword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (402038h)]
}
0040104B xor eax,eax
0040104D pop ecx
0040104E ret
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.
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
foo
? Tidakkah penguncian utas 1 this.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.
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.
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
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:
private static int _flag = 0;
private static int _value = 0;
var t1 = Task.Run(() =>
{
_value = 10; /* compiler could switch these lines */
_flag = 5;
});
var t2 = Task.Run(() =>
{
if (_flag == 5)
{
Console.WriteLine("Value: {0}", _value);
}
});
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.
private static volatile int _flag = 0;
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.
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.
beberapa utas dapat mengakses suatu variabel. Pembaruan terbaru ada pada variabel