Pertanyaan Kebutuhan pengubah volatil dalam penguncian yang ditandai ganda di .NET


Beberapa teks mengatakan bahwa ketika menerapkan penguncian ganda di. NET bidang Anda mengunci harus memiliki pengubah yang mudah menguap diterapkan. Tapi mengapa tepatnya? Mempertimbangkan contoh berikut:

public sealed class Singleton
{
   private static volatile Singleton instance;
   private static object syncRoot = new Object();

   private Singleton() {}

   public static Singleton Instance
   {
      get 
      {
         if (instance == null) 
         {
            lock (syncRoot) 
            {
               if (instance == null) 
                  instance = new Singleton();
            }
         }

         return instance;
      }
   }
}

mengapa tidak "mengunci (syncRoot)" mencapai konsistensi memori yang diperlukan? Bukankah benar bahwa setelah pernyataan "kunci", baik membaca dan menulis akan bergejolak dan konsistensi yang diperlukan akan tercapai?


76
2017-12-27 00:13


asal


Jawaban:


Volatile tidak diperlukan. Yah, semacam **

volatile digunakan untuk membuat penghalang memori * antara membaca dan menulis pada variabel.
lock, bila digunakan, menyebabkan hambatan memori dibuat di sekitar blok di dalam lock, selain membatasi akses ke blok ke satu utas.
Penghalang memori membuatnya sehingga setiap thread membaca nilai terkini dari variabel (bukan nilai lokal yang di-cache di beberapa register) dan bahwa compiler tidak menyusun ulang pernyataan. Menggunakan volatile tidak perlu ** karena Anda sudah punya kunci.

Joseph Albahari menjelaskan hal ini dengan cara yang lebih baik daripada yang pernah saya bisa.

Dan pastikan untuk memeriksa Jon Skeet panduan untuk menerapkan tunggal di C #


memperbarui:
*volatile menyebabkan pembacaan variabel menjadi VolatileReaddan menulis untuk menjadi VolatileWrites, yang pada x86 dan x64 pada CLR, diimplementasikan dengan MemoryBarrier. Mereka mungkin lebih halus pada sistem lain.

** Jawaban saya hanya benar jika Anda menggunakan CLR pada prosesor x86 dan x64. Saya t mungkin benar dalam model memori lain, seperti pada Mono (dan implementasi lainnya), Itanium64 dan perangkat keras masa depan. Inilah yang disebut Jon dalam artikelnya di "gotchas" untuk penguncian ganda.

Melakukan salah satu dari {menandai variabel sebagai volatile, membacanya dengan Thread.VolatileRead, atau menyisipkan panggilan ke Thread.MemoryBarrier} mungkin diperlukan agar kode berfungsi dengan baik dalam situasi model memori yang lemah.

Dari apa yang saya pahami, di CLR (bahkan pada IA64), menulis tidak pernah tertata ulang (menulis selalu merilis semantik). Namun, pada IA64, bacaan dapat disusun kembali sebelum ditulis, kecuali jika ditandai bertanda volatil. Sayangnya, saya tidak memiliki akses ke perangkat keras IA64 untuk dimainkan, jadi apa pun yang saya katakan tentang itu akan menjadi spekulasi.

Saya juga menemukan artikel ini bermanfaat:
http://www.codeproject.com/KB/tips/MemoryBarrier.aspx
artikel vance morrison (semuanya terkait dengan ini, ini berbicara tentang penguncian yang dicentang ganda)
artikel chris brumme  (semuanya terkait dengan ini)
Joe Duffy: Rusak Variasi Penguncian Terdaftar Ganda 

Serial luis abreu tentang multithreading memberikan gambaran yang bagus tentang konsep-konsep juga
http://msmvps.com/blogs/luisabreu/archive/2009/06/29/multithreading-load-and-store-reordering.aspx
http://msmvps.com/blogs/luisabreu/archive/2009/07/03/multithreading-introducing-memory-fences.aspx 


57
2017-12-27 01:06



Ada cara untuk menerapkannya tanpa volatilebidang. Saya akan menjelaskannya ...

Saya pikir itu adalah pengubahan akses memori di dalam kunci yang berbahaya, sehingga Anda bisa mendapatkan contoh yang tidak diselesaikan secara sempurna di luar kunci. Untuk menghindari ini saya melakukan ini:

public sealed class Singleton
{
   private static Singleton instance;
   private static object syncRoot = new Object();

   private Singleton() {}

   public static Singleton Instance
   {
      get 
      {
         // very fast test, without implicit memory barriers or locks
         if (instance == null)
         {
            lock (syncRoot)
            {
               if (instance == null)
               {
                    var temp = new Singleton();

                    // ensures that the instance is well initialized,
                    // and only then, it assigns the static variable.
                    System.Threading.Thread.MemoryBarrier();
                    instance = temp;
               }
            }
         }

         return instance;
      }
   }
}

Memahami kode

Bayangkan bahwa ada beberapa kode inisialisasi di dalam konstruktor kelas Singleton. Jika instruksi ini disusun kembali setelah bidang diatur dengan alamat objek baru, maka Anda memiliki contoh yang tidak lengkap ... bayangkan bahwa kelas memiliki kode ini:

private int _value;
public int Value { get { return this._value; } }

private Singleton()
{
    this._value = 1;
}

Sekarang bayangkan panggilan ke konstruktor menggunakan operator baru:

instance = new Singleton();

Ini dapat diperluas ke operasi ini:

ptr = allocate memory for Singleton;
set ptr._value to 1;
set Singleton.instance to ptr;

Bagaimana jika saya menyusun ulang instruksi ini seperti ini:

ptr = allocate memory for Singleton;
set Singleton.instance to ptr;
set ptr._value to 1;

Apakah ada bedanya? TIDAK jika Anda memikirkan satu utas. IYA NIH jika Anda memikirkan beberapa utas ... bagaimana jika benang terputus tepat setelahnya set instance to ptr:

ptr = allocate memory for Singleton;
set Singleton.instance to ptr;
-- thread interruped here, this can happen inside a lock --
set ptr._value to 1; -- Singleton.instance is not completelly initialized

Itulah yang menghambat penghalang memori, dengan tidak mengizinkan pengubahan ulang akses memori:

ptr = allocate memory for Singleton;
set temp to ptr; // temp is a local variable (that is important)
set ptr._value to 1;
-- memory barrier... cannot reorder writes after this point, or reads before it --
-- Singleton.instance is still null --
set Singleton.instance to temp;

Selamat coding!


31
2017-10-18 00:38



Saya tidak berpikir ada yang benar-benar menjawab pertanyaan, jadi saya akan mencobanya.

Yang mudah menguap dan yang pertama if (instance == null) tidak "diperlukan". Kunci akan membuat kode ini aman.

Jadi pertanyaannya adalah: mengapa Anda menambahkan yang pertama if (instance == null)?

Alasannya adalah mungkin untuk menghindari mengeksekusi bagian kode yang terkunci tidak perlu. Saat Anda mengeksekusi kode di dalam kunci, setiap untaian lain yang mencoba juga mengeksekusi kode tersebut diblokir, yang akan memperlambat program Anda jika Anda mencoba mengakses singleton itu sering dari banyak utas. Tergantung pada bahasa / platform, bisa juga ada overhead dari kunci itu sendiri yang ingin Anda hindari.

Jadi cek null pertama ditambahkan sebagai cara yang sangat cepat untuk melihat apakah Anda memerlukan kunci. Jika Anda tidak perlu membuat singleton, Anda dapat menghindari kunci sepenuhnya.

Tetapi Anda tidak dapat memeriksa apakah referensi tersebut null tanpa menguncinya dengan cara tertentu, karena karena caching prosesor, untaian lain dapat mengubahnya dan Anda akan membaca nilai "basi" yang akan mengarahkan Anda untuk memasukkan kunci yang tidak perlu. Tapi kamu mencoba untuk menghindari kunci!

Jadi Anda membuat singile volatile untuk memastikan bahwa Anda membaca nilai terbaru, tanpa perlu menggunakan kunci.

Anda masih memerlukan kunci dalam karena mudah menguap hanya melindungi Anda selama satu akses ke variabel - Anda tidak dapat menguji dan mengaturnya dengan aman tanpa menggunakan kunci.

Sekarang, apakah ini benar-benar berguna?

Yah aku akan mengatakan "dalam banyak kasus, tidak ada".

Jika Singleton.Instansi bisa menyebabkan inefisiensi karena kunci, lalu mengapa Anda memanggilnya begitu sering sehingga ini akan menjadi masalah yang signifikan? Inti dari tunggal adalah bahwa hanya ada satu, sehingga kode Anda dapat membaca dan menyimpan referensi singel satu kali.

Satu-satunya kasus yang dapat saya pikirkan di mana caching ini tidak akan mungkin adalah ketika Anda memiliki sejumlah besar thread (misalnya server menggunakan thread baru untuk memproses setiap permintaan bisa menciptakan jutaan thread yang sangat singkat, masing-masing yang harus memanggil Singleton. Sekali lagi).

Jadi saya menduga bahwa penguncian yang diperiksa ganda adalah mekanisme yang memiliki tempat nyata dalam kasus-kasus kinerja-kritis yang sangat spesifik, dan kemudian setiap orang telah memanjat pada "ini adalah cara yang tepat untuk melakukannya" ikut-ikutan tanpa benar-benar memikirkan apa yang dilakukannya dan apakah itu akan benar-benar diperlukan dalam hal mereka menggunakannya untuk.


7
2017-12-27 08:55



AFAIK (dan - bawa ini dengan hati-hati, saya tidak melakukan banyak hal bersamaan) tidak. Kunci hanya memberi Anda sinkronisasi antara beberapa pesaing (utas).

volatile di sisi lain memberitahu mesin Anda untuk mengevaluasi kembali nilai setiap kali, sehingga Anda tidak tersandung pada nilai cache (dan salah).

Lihat http://msdn.microsoft.com/en-us/library/ms998558.aspx dan perhatikan kutipan berikut:

Juga, variabel dinyatakan stabil untuk memastikan bahwa penugasan ke variabel instan selesai sebelum variabel instan dapat diakses.

Penjelasan tentang volatile: http://msdn.microsoft.com/en-us/library/x13ttww7%28VS.71%29.aspx


3
2017-12-27 00:20



Anda harus menggunakan volatile dengan pola kunci cek ganda.

Kebanyakan orang mengacu pada artikel ini sebagai bukti bahwa Anda tidak perlu mudah menguap: https://msdn.microsoft.com/en-us/magazine/cc163715.aspx#S10

Tetapi mereka gagal membaca sampai akhir: "A Final Word of Warning - Saya hanya menebak pada model memori x86 dari perilaku yang diamati pada prosesor yang ada. Dengan demikian teknik low-lock juga rentan karena perangkat keras dan kompiler bisa menjadi lebih agresif dari waktu ke waktu. Berikut adalah beberapa strategi untuk meminimalkan dampak kerapuhan ini pada kode Anda. Pertama, bila memungkinkan, hindari teknik low-lock. (...) Akhirnya, asumsikan model memori terlemah mungkin, menggunakan deklarasi yang mudah menguap daripada bergantung pada jaminan implisit. "

Jika Anda perlu lebih meyakinkan, baca artikel ini tentang spesifikasi ECMA yang akan digunakan untuk platform lain: msdn.microsoft.com/en-us/magazine/jj863136.aspx

Jika Anda perlu meyakinkan lebih lanjut, baca artikel baru ini bahwa optimisasi dapat dimasukkan yang mencegahnya bekerja tanpa volatile: msdn.microsoft.com/en-us/magazine/jj883956.aspx

Singkatnya "mungkin" bekerja untuk Anda tanpa volatile untuk saat ini, tetapi jangan kebetulan menulis kode yang tepat dan baik menggunakan metode volatile atau volatileread / write. Artikel yang menyarankan untuk melakukan yang sebaliknya terkadang meninggalkan beberapa kemungkinan risiko pengoptimalan JIT / kompilator yang dapat memengaruhi kode Anda, serta kami optimisasi yang mungkin terjadi di masa mendatang yang dapat merusak kode Anda. Juga sebagaimana disebutkan asumsi dalam artikel sebelumnya, asumsi sebelumnya tentang bekerja tanpa volatile mungkin tidak berlaku pada ARM.


3
2018-02-23 04:18



Itu lock Cukup. Spesifikasi bahasa MS (3.0) sendiri menyebutkan skenario yang tepat ini dalam §8.12, tanpa menyebutkan apa pun volatile:

Pendekatan yang lebih baik adalah melakukan sinkronisasi   akses ke data statis dengan mengunci a   objek statis pribadi. Sebagai contoh:

class Cache
{
    private static object synchronizationObject = new object();
    public static void Add(object x) {
        lock (Cache.synchronizationObject) {
          ...
        }
    }
    public static void Remove(object x) {
        lock (Cache.synchronizationObject) {
          ...
        }
    }
}

2
2017-12-27 09:19



Saya pikir saya telah menemukan apa yang saya cari. Detail ada di artikel ini - http://msdn.microsoft.com/en-us/magazine/cc163715.aspx#S10.

Untuk meringkas -. NET pengubah volatile memang tidak diperlukan dalam situasi ini. Namun dalam model memori yang lebih lemah, penulisan yang dibuat dalam konstruktor objek yang diprakarsai dengan malas mungkin ditunda setelah menulis ke lapangan, sehingga utas lainnya mungkin membaca contoh kosong non-null dalam pernyataan if pertama.


2
2017-12-27 22:56



Ini posting yang cukup bagus tentang penggunaan volatile dengan penguncian yang ditandai ganda:

http://tech.puredanger.com/2007/06/15/double-checked-locking/

Di Java, jika tujuannya adalah untuk melindungi variabel Anda tidak perlu mengunci jika ditandai sebagai volatile


-2
2017-12-27 00:19