it-swarm-id.com

Kapan kata kunci volatil digunakan dalam C #?

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 itu akan menghemat penggunaan penguncian?

279
Doron Yaacoby

Saya tidak berpikir ada orang yang lebih baik untuk menjawab ini daripada Eric Lippert (penekanan pada aslinya): 

Di C #, "volatile" berarti tidak hanya "memastikan bahwa kompiler dan Jitter tidak melakukan pengurutan ulang kode atau register caching .__ optimasi pada variabel ini". Ini juga berarti "beri tahu prosesor untuk Melakukan apa pun yang perlu mereka lakukan untuk memastikan bahwa saya membaca Nilai terakhir, bahkan jika itu berarti menghentikan prosesor lain dan membuat Mereka menyinkronkan memori utama dengan cache ".

Sebenarnya, bagian terakhir itu bohong. Semantik sebenarnya dari volatile berbunyi dan menulis jauh lebih kompleks daripada yang saya uraikan di sini; di Bahkan mereka tidak benar-benar menjamin bahwa setiap prosesor berhenti apa sedang melakukan dan memperbarui cache ke/dari memori utama. Sebaliknya, mereka menyediakan jaminan yang lebih lemah tentang bagaimana memori mengakses sebelum dan sesudah membaca dan menulis dapat diamati untuk dipesan dengan memperhatikan satu sama lain . Operasi tertentu seperti membuat utas baru, memasukkan kunci, atau menggunakan salah satu dari keluarga metode yang saling terkait memperkenalkan lebih kuat. jaminan tentang pengamatan pemesanan. Jika Anda ingin detail lebih lanjut, baca bagian 3.10 dan 10.5.3 dari spesifikasi C # 4.0.

Terus terang, Saya tidak menyarankan Anda membuat bidang yang mudah berubah. Volatile bidang adalah tanda bahwa Anda melakukan sesuatu yang benar-benar gila: Anda berusaha membaca dan menulis nilai yang sama pada dua utas berbeda tanpa meletakkan kunci di tempatnya. Kunci menjamin memori dibaca atau dimodifikasi di dalam kunci diamati konsisten, jaminan kunci bahwa hanya satu utas yang mengakses sepotong memori yang diberikan pada satu waktu, dan sebagainya di. Jumlah situasi di mana kunci terlalu lambat sangat kecil, dan kemungkinan Anda akan mendapatkan kode yang salah karena Anda tidak mengerti model memori yang tepat sangat besar. SAYA jangan mencoba menulis kode kunci-rendah apa pun kecuali untuk yang paling sepele penggunaan operasi yang saling terkait. Saya membiarkan penggunaan "volatile" menjadi ahli nyata.

Untuk bacaan lebih lanjut, lihat:

254
Ohad Schneider

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 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 inti 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. Kompiler 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 (menambahkan , 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, data tidak akan pernah rusak, bahkan dengan memori yang tidak selaras.

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 perlu data 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 dikandung untuk digunakan dengan operasi IO di mana IO akan terus berubah tetapi memiliki alamat yang konstan, seperti perangkat yang dipetakan memori UART, dan kompiler tidak boleh tetap 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.

54
Skizz

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:

  1. Thread 1 menanyakan apakah suatu variabel adalah null . // jika (this.foo == null)
  2. Thread 1 menentukan variabelnya nol, jadi masukkan kunci . // kunci (this.bar)
  3. Thread 1 bertanya LAGI apakah variabelnya null . // if (this.foo == null)
  4. Thread 1 masih menentukan variabelnya nol, jadi ia memanggil konstruktor dan memberikan nilai ke variabel . // this.foo = new Foo ();

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:

  1. Thread 2 bertanya apakah variabelnya nol. // jika (this.foo == null)
  2. Thread 2 menentukan variabel BUKAN nol, jadi cobalah untuk menggunakannya . // this.foo.MakeFoo ()

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 Dobel, dan secara singkat menyentuh topik ini: http://en.wikipedia.org/wiki/Double-checked_locking

38
AndrewTek

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.

22
Dr. Bob

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 pada kompiler "Saya ingin Anda menyimpan nilai ini dalam memori". Ini menjamin bahwa utas ke-2 mengambil nilai terbaru.

20
Benoit

CLR suka 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.

13
Joseph Daigle

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 dieksekusi, bisa jadi _flag memiliki nilai 5, tetapi _value memiliki 0. Jadi logika yang diharapkan bisa dipatahkan. 

Untuk memperbaikinya, Anda dapat menggunakan volatile kata kunci yang dapat Anda terapkan pada 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 itu menonaktifkan optimasi kompiler tertentu, itu akan merusak kinerja. Ini juga tidak didukung oleh semua bahasa .NET (Visual Basic tidak mendukungnya), sehingga menghambat interoperabilitas bahasa.

0
Aliaksei Maniuk

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 lebih berbahaya daripada baik jika digunakan secara tidak perlu. YAITU. Jangan pernah menggunakannya. TETAPI di versi runtime yang lebih lama, itu IS diperlukan untuk mengunci pemeriksaan ganda yang benar pada bidang statis. Khusus bidang statis yang kelasnya memiliki kode inisialisasi kelas statis.

0
Paul Easter