.NET'te çift denetimli kilitlemede uçucu değiştirici ihtiyacı


85

Birden çok metin, .NET'te çift denetimli kilitlemeyi uygularken kilitlediğiniz alana geçici değiştiricinin uygulanmış olması gerektiğini söyler. Ama neden tam olarak? Aşağıdaki örneği ele alarak:

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

neden "kilitlemek (syncRoot)" gerekli bellek tutarlılığını sağlamıyor? "Kilit" ifadesinden sonra hem okuma hem de yazmanın uçucu olacağı ve böylece gerekli tutarlılığın sağlanacağı doğru değil mi?


2
Bu zaten birçok kez çiğnendi. yoda.arachsys.com/csharp/singleton.html
Hans Passant

1
Ne yazık ki, Jon bu makalede iki kez 'uçucu'ya atıfta bulunmakta ve hiçbir referans doğrudan verdiği kod örneklerine değinmemektedir.
Dan Esparza

Endişeyi anlamak için bu makaleye bakın: igoro.com/archive/volatile-keyword-in-c-memory-model-explained Temelde JIT'in örnek değişkeni için bir CPU kaydı kullanması TEORİK OLARAK mümkündür - özellikle bir orada biraz ekstra kod var. Bu nedenle, bir if ifadesinin potansiyel olarak iki kez yapılması, başka bir iş parçacığında değişmesine bakılmaksızın aynı değeri döndürebilir. Gerçekte cevap biraz karmaşıktır, kilit ifadesi burada işleri daha iyi hale getirmekten sorumlu olabilir veya olmayabilir (devam)
user2685937

(devam eden önceki yoruma bakın) - İşte gerçekten olduğunu düşündüğüm şey - Temel olarak, okumaktan veya bir değişken ayarlamaktan daha karmaşık bir şey yapan herhangi bir kod JIT'i tetikleyebilir, bunu optimize etmeye çalışmayı unutun, sadece yükleyip belleğe kaydedelim Bir işlev çağrılırsa, JIT her seferinde doğrudan bellekten yazıp okumak yerine, her seferinde orada depolanmışsa, kaydı kaydetme ve yeniden yükleme ihtiyacı duyacaktır. Kilidin özel bir şey olmadığını nasıl bilebilirim?
Igor'dan

(yukarıdaki 2 yoruma bakın) - Igor'un kodunu test ettim ve yeni bir iş parçacığı oluşturduğunda etrafına bir kilit ekledim ve hatta döngü haline getirdim. Örnek değişkeni döngüden kaldırıldığı için yine de kodun çıkmasına neden olmaz. While döngüsüne basit bir yerel değişken kümesi eklemek, değişkeni döngüden kaldırmaya devam etti - Şimdi, ifadeler veya bir yöntem çağrısı veya evet bir kilit çağrısı gibi daha karmaşık herhangi bir şey optimizasyonu engeller ve böylece çalışmasını sağlar. Bu nedenle, herhangi bir karmaşık kod, JIT'in optimize etmesine izin vermek yerine genellikle doğrudan değişken erişimi zorlar. (sonraki yoruma devam)
user2685937

Yanıtlar:


60

Uçucu gereksizdir. İyi sıralama**

volatiledeğişken üzerindeki okuma ve yazma işlemleri arasında bir bellek engeli * oluşturmak için kullanılır.
lock, kullanıldığında, bloğa lockerişimi bir iş parçacığıyla sınırlandırmanın yanı sıra, içindeki bloğun çevresinde bellek engellerinin oluşturulmasına neden olur .
Bellek engelleri, her iş parçacığının değişkenin en güncel değerini (bazı kayıtlarda önbelleğe alınan yerel bir değeri değil) okumasını ve derleyicinin ifadeleri yeniden sıralamamasını sağlar. Kullanmak volatilegereksiz ** çünkü zaten bir kilidiniz var.

Joseph Albahari bu konuyu benim yapabileceğimden çok daha iyi açıklıyor.

Ve Jon Skeet'in singleton'u C # 'ta uygulama kılavuzuna göz atmayı unutmayın


update :
* volatiledeğişkenin VolatileReads olmasına neden olur VolatileWriteve CLR üzerindeki x86 ve x64 üzerinde bir MemoryBarrier. Diğer sistemlerde daha ince taneli olabilirler.

** Cevabım yalnızca CLR'yi x86 ve x64 işlemcilerde kullanıyorsanız doğrudur. Bu olabilir Mono (ve diğer uygulamaları), Itanium64 ve gelecekteki donanım üzerinde olduğu gibi, diğer bellek modellerinde gerçek olamayacak. Bu, Jon'un çift kontrollü kilitleme için "gotchas" makalesinde bahsettiği şeydir.

Kodun zayıf bir bellek modeli durumunda düzgün çalışması için {değişkeni işaretlemek, volatileonunla okumak Thread.VolatileReadveya bir çağrı eklemek Thread.MemoryBarrier} işlemlerinden birini yapmak gerekli olabilir.

Anladığım kadarıyla, CLR'de (IA64'te bile), yazılar asla yeniden sıralanmıyor (yazılar her zaman yayın anlamlarına sahiptir). Bununla birlikte, IA64'te, değişken olarak işaretlenmedikçe, okumalar yazma işlemlerinden önce gelecek şekilde yeniden sıralanabilir. Ne yazık ki, oynamak için IA64 donanımına erişimim yok, bu yüzden bu konuda söyleyeceğim her şey spekülasyon olurdu.

Bu makaleleri de yararlı buldum:
http://www.codeproject.com/KB/tips/MemoryBarrier.aspx
vance morrison'un makalesi (buna her şey bağlantılıdır, iki kez kontrol edilmiş kilitlemeden
bahseder ) chris brumme'nin makalesi (her şey buna bağlantılıdır) )
Joe Duffy: Çift Kontrollü Kilitlemenin Kırık Çeşitleri

luis abreu'nun multithreading serisi, kavramlara da güzel bir genel bakış sağlar
http://msmvps.com/blogs/luisabreu/archive/2009/06/29/multithreading-load-and-store-reordering.aspx
http: // msmvps. com / bloglar / luisabreu / arşiv / 2009/07/03 / multithreading-tanıtım-bellek-çitleri.aspx


Jon Skeet, uygun bellek bariyeri oluşturmak için uçucu değiştiricinin gerekli olduğunu söylerken, ilk bağlantı yazarı kilidin (Monitor.Enter) yeterli olacağını söylüyor. Aslında kim haklı ???
Konstantin

@Konstantin, Jon'un Itanium 64 işlemcilerdeki bellek modeline atıfta bulunduğu görülüyor, bu nedenle bu durumda uçucu kullanmak gerekli olabilir. Ancak, x86 ve x64 işlemcilerde uçucu gereksizdir. Birazdan daha fazla güncelleme yapacağım.
dan

Kilit gerçekten bir bellek engeli oluşturuyorsa ve bellek barier gerçekten hem komut sırası HEM DE önbelleği geçersiz kılmakla ilgiliyse, tüm işlemcilerde çalışmalıdır. Her neyse, bu kadar basit bir şeyin bu kadar kafa karışıklığına neden olması çok garip ...
Konstantin

2
Bu cevap bana yanlış görünüyor. Eğer volatileüzerinde gereksiz herhangi bir platformda sonra JIT bellek yükleri optimize olamazdı anlamına geleceğini object s1 = syncRoot; object s2 = syncRoot;için object s1 = syncRoot; object s2 = s1;bu platformda. Bu bana pek olası görünmüyor.
user541686

1
CLR yazımları yeniden sıralamasaydı bile (bunun doğru olduğundan şüpheliyim, bunu yaparak birçok çok iyi optimizasyon yapılabilir), yapıcı çağrısını satır içi yapabildiğimiz ve nesneyi yerinde oluşturabildiğimiz sürece hala hatalı olabilir. (yarı başlatılmış bir nesne görebiliyorduk). Temel CPU'nun kullandığı herhangi bir bellek modelinden bağımsız olarak! Eric Lippert'e göre Intel'deki CLR, en azından bu optimizasyonu reddeden kuruculardan sonra bir membran taşıyıcı sunuyor, ancak bu şartname gerektirmiyor ve örneğin
ARM'de

34

volatileAlan olmadan uygulamanın bir yolu var . Açıklayacağım ...

Bence tehlikeli olan, kilidin dışında tamamen başlatılmamış bir örneği elde edebilmeniz için, kilit içinde bellek erişiminin yeniden sıralanmasıdır. Bundan kaçınmak için şunu yapıyorum:

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

Kodu anlamak

Singleton sınıfının yapıcısının içinde bazı başlatma kodlarının olduğunu hayal edin. Alan yeni nesnenin adresiyle ayarlandıktan sonra bu talimatlar yeniden sıralanırsa, tamamlanmamış bir örneğiniz var ... Sınıfın şu koda sahip olduğunu hayal edin:

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

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

Şimdi yeni operatörü kullanarak kurucuya bir çağrı hayal edin:

instance = new Singleton();

Bu, şu işlemlere genişletilebilir:

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

Ya bu talimatları şu şekilde yeniden sıralarsam:

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

Bir fark yaratır mı? Tek bir iş parçacığı düşünüyorsanız HAYIR . EVET Eğer birden fazla iş parçacığı düşünüyorsanız ... ya iş parçacığı hemen sonra kesilirse 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

Bellek erişiminin yeniden düzenlenmesine izin vermeyerek bellek engelinin önlediği şey budur:

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;

Mutlu kodlamalar!


1
CLR, başlatılmadan önce bir nesneye erişime izin veriyorsa, bu bir güvenlik açığıdır. Yalnızca genel yapıcısı "SecureMode = 1" ayarlayan ve sonra örnek yöntemleri bunu kontrol eden ayrıcalıklı bir sınıf düşünün. Yapıcı çalışmadan bu örnek yöntemlerini çağırabilirseniz, güvenlik modelinden çıkabilir ve korumalı alanı ihlal edebilirsiniz.
MichaelGG

1
@MichaelGG: açıkladığınız durumda, bu sınıf ona erişmek için birden fazla iş parçacığını destekleyecekse, o zaman bu bir problemdir. Yapıcı çağrısı seğirme tarafından satır içine alınmışsa, o zaman CPU'nun talimatları, depolanan referansın tamamen başlatılmamış bir örneğe işaret edecek şekilde yeniden sıralaması mümkündür. Bu bir CLR güvenlik sorunu değildir, çünkü önlenebilirdir, programcının sorumluluğundadır: bu tür bir sınıfın yapıcısı içinde birbirine kenetlenmiş, bellek engelleri, kilitler ve / veya geçici alanlar.
Miguel Angelo

2
Ktorun içindeki bir bariyer onu düzeltmez. CLR, ctor tamamlanmadan önce yeni tahsis edilen nesneye referansı atarsa ​​ve bir membran taşıyıcı yerleştirmezse, başka bir iş parçacığı yarı başlatılmış bir nesne üzerinde bir örnek yöntemi yürütebilir.
MichaelGG

Bu, ReSharper 2016/2017'nin C #'da bir DCL olması durumunda önerdiği "alternatif model" dir. Otoh Java yapar sonucu olduğunu garanti new.. tam olarak başlatılmadan
user2864740

MS .net uygulamasının, yapılandırıcının sonuna bir bellek engeli yerleştirdiğini biliyorum ... ama üzgün olmaktansa güvenli olması daha iyidir.
Miguel Angelo

7

Kimsenin soruyu gerçekten yanıtladığını sanmıyorum , bu yüzden bir deneyeceğim.

Uçucu ve ilk if (instance == null)"gerekli" değildir. Kilit, bu kodu iş parçacığı için güvenli hale getirecektir.

Öyleyse soru şu: neden ilkini eklersiniz if (instance == null)?

Bunun nedeni muhtemelen kodun kilitli bölümünü gereksiz yere yürütmekten kaçınmaktır. Kilidin içindeki kodu çalıştırırken, bu kodu da yürütmeye çalışan diğer herhangi bir iş parçacığı engellenir, bu da birçok iş parçacığından sık sık singleton'a erişmeye çalışırsanız programınızı yavaşlatır. Dile / platforma bağlı olarak, kilidin kendisinden kaçınmak isteyebileceğiniz genel giderler de olabilir.

Bu nedenle, kilide ihtiyacınız olup olmadığını görmenin gerçekten hızlı bir yolu olarak ilk sıfır kontrolü eklenir. Tekli oluşturmanıza gerek yoksa, kilidi tamamen önleyebilirsiniz.

Ancak, referansın bir şekilde kilitlemeden boş olup olmadığını kontrol edemezsiniz, çünkü işlemci önbelleğe alma nedeniyle başka bir iş parçacığı onu değiştirebilir ve gereksiz yere kilide girmenize neden olacak "eski" bir değer okuyabilirsiniz. Ama kilitlenmekten kaçınmaya çalışıyorsun!

Böylece, kilit kullanmaya gerek kalmadan en son değeri okumanızı sağlamak için tekliyi uçucu hale getirirsiniz.

Yine de iç kilide ihtiyacınız var çünkü volatile yalnızca değişkene tek bir erişim sırasında sizi korur - bir kilit kullanmadan güvenli bir şekilde test edip ayarlayamazsınız.

Şimdi, bu gerçekten faydalı mı?

Pekala, "çoğu durumda hayır" derdim.

Singleton.Instance kilitler nedeniyle verimsizliğe neden olabiliyorsa, o zaman neden bu kadar sık ​​çağırıyorsunuz ki bu önemli bir sorun olur ? Bir singletonun tüm amacı, yalnızca bir tane olmasıdır, böylece kodunuz singleton referansını bir kez okuyabilir ve önbelleğe alabilir.

Bu önbelleğe almanın nerede mümkün olmayacağını düşünebildiğim tek durum, çok sayıda iş parçacığına sahip olduğunuzda olabilir (örneğin, her isteği işlemek için yeni bir iş parçacığı kullanan bir sunucu, her biri çok kısa çalışan milyonlarca iş parçacığı oluşturuyor olabilir) Singleton.Instance'ı bir kez çağırmak zorunda kalacaktı).

Bu yüzden, çifte kontrollü kilitlemenin performans açısından kritik çok özel durumlarda gerçek bir yeri olan bir mekanizma olduğundan şüpheleniyorum ve sonra herkes gerçekten ne yaptığını ve yapıp yapmadığını düşünmeden "bunu yapmanın doğru yolu bu" ana vagonuna takıldı. aslında kullanmaları durumunda gerekli olacaktır.


6
Bu yanlış ile noktayı kaçırmak arasında bir yerdedir. volatileçift ​​kontrol edilmiş kilitlemede kilit semantiğiyle hiçbir ilgisi yoktur, bellek modeli ve önbellek tutarlılığı ile ilgisi vardır. Amacı, bir iş parçacığının başka bir iş parçacığı tarafından başlatılmakta olan bir değeri almamasını sağlamaktır; bu, çift kontrol kilidi modelinin doğası gereği engellemez. Java'da kesinlikle volatileanahtar kelimeye ihtiyacınız var ; .NET'te bulanık, çünkü ECMA'ya göre yanlış ama çalışma zamanına göre doğru. Her iki durumda da lockkesinlikle yok değil icabına.
Aaronaught

Huh? İfadenizin söylediklerimle nerede uyuşmadığını göremiyorum ve uçucunun herhangi bir şekilde kilit anlambilimiyle ilişkili olduğunu söylemedim.
Jason Williams

6
Cevabınız, bu başlıktaki diğer birkaç ifade gibi lock, kodun iş parçacığını güvenli hale getirdiğini iddia ediyor . Bu kısım doğrudur, ancak çift kontrol kilidi modeli onu güvensiz hale getirebilir . Eksik göründüğün şey bu. Bu yanıt, nedeni olan iş parçacığı güvenlik sorunlarına hiç değinmeden bir çift kontrol kilidinin anlamı ve amacı hakkında dolambaçlı görünüyor volatile.
Aaronaught

1
İle instanceişaretlenmişse nasıl güvensiz hale getirilebilir volatile?
UserControl

5

Çift kontrol kilidi deseni ile uçucu kullanmalısınız.

Çoğu kişi bu makaleyi, uçucuya ihtiyacınız olmadığının kanıtı olarak gösteriyor: https://msdn.microsoft.com/en-us/magazine/cc163715.aspx#S10

Ancak sonuna kadar okuyamıyorlar: " Son Bir Uyarı Sözü - Sadece x86 bellek modelinde mevcut işlemcilerde gözlemlenen davranıştan tahmin ediyorum. Bu nedenle düşük kilit teknikleri de kırılgandır çünkü donanım ve derleyiciler zaman içinde daha agresifleşebilir. . Bu kırılganlığın kodunuz üzerindeki etkisini en aza indirmek için bazı stratejiler. İlk olarak, mümkün olduğunda düşük kilit tekniklerinden kaçının. (...) Son olarak, örtük garantilere güvenmek yerine geçici bildirimler kullanarak mümkün olan en zayıf bellek modelini varsayın. . "

Daha ikna etmeye ihtiyacınız varsa, ECMA spesifikasyonundaki bu makaleyi okuyun diğer platformlar için kullanılacaktır: msdn.microsoft.com/en-us/magazine/jj863136.aspx

Daha fazla ikna etmeye ihtiyacınız varsa, bu yeni makaleyi, geçici olmadan çalışmasını engelleyen optimizasyonların konulabileceğini okuyun: msdn.microsoft.com/en-us/magazine/jj883956.aspx

Özetle, şu an için geçici olmadan sizin için "işe yarayabilir", ancak uygun kod yazma şansına sahip olun ve ya uçucu ya da uçucu / yazım yöntemlerini kullanın. Aksi şekilde yapılmasını öneren makaleler, bazen kodunuzu etkileyebilecek JIT / derleyici optimizasyonlarının bazı olası risklerini ve ayrıca kodunuzu bozabilecek gelecekteki optimizasyonları dışarıda bırakıyor. Ayrıca, son makalede bahsedilen varsayımlar gibi, değişken olmadan çalışmanın önceki varsayımları zaten ARM üzerinde geçerli olmayabilir.


1
İyi cevap. Bu soruya verilebilecek tek doğru cevap basit "Hayır" dır. Buna göre kabul edilen cevap yanlıştır.
Dennis Kassel

3

AFAIK (ve - bunu dikkatli bir şekilde al, aynı anda pek çok şey yapmıyorum) hayır. Kilit size birden fazla yarışmacı (konu) arasında senkronizasyon sağlar.

Öte yandan volatile, makinenize değeri her seferinde yeniden değerlendirmesini söyler, böylece önbelleğe alınmış (ve yanlış) bir değere rastlamazsınız.

Http://msdn.microsoft.com/en-us/library/ms998558.aspx sayfasına bakın ve aşağıdaki alıntıya dikkat edin:

Ayrıca, değişkene erişilmeden önce örnek değişkenine atamanın tamamlanmasını sağlamak için değişkenin geçici olduğu bildirilir.

Uçucu bir açıklama: http://msdn.microsoft.com/en-us/library/x13ttww7%28VS.71%29.aspx


2
Bir 'kilit' aynı zamanda uçucu ile aynı (veya ondan daha iyi) bir bellek engeli sağlar.
Henk Holterman

2

Sanırım aradığımı buldum. Ayrıntılar bu makalededir - http://msdn.microsoft.com/en-us/magazine/cc163715.aspx#S10 .

Özetlemek gerekirse - .NET'te uçucu değiştiriciye gerçekten de bu durumda gerek yoktur. Ancak daha zayıf bellek modellerinde, tembel olarak başlatılan nesnenin yapıcısında yapılan yazmalar, alana yazıldıktan sonra gecikebilir, bu nedenle diğer evreler, ilk if ifadesinde bozuk boş olmayan örneği okuyabilir.


1
Bu makalenin en altında dikkatle okuyun, özellikle de yazar son cümle: "Son Bir Uyarı Sözü - Yalnızca x86 bellek modelinde mevcut işlemcilerde gözlemlenen davranıştan tahmin ediyorum. Bu nedenle düşük kilit teknikleri de kırılgandır çünkü donanım ve derleyiciler zamanla daha agresif hale gelebilir. İşte bu kırılganlığın kodunuz üzerindeki etkisini en aza indirmek için bazı stratejiler. İlk olarak, mümkün olduğunca düşük kilit tekniklerinden kaçının. (...) Son olarak, mümkün olan en zayıf bellek modelini varsayın, Örtük garantilere güvenmek yerine geçici beyanlar kullanmak. "
user2685937

1
Daha fazla ikna etmeye ihtiyacınız varsa, ECMA spesifikasyonundaki bu makaleyi okuyun, diğer platformlar için kullanılacaktır: msdn.microsoft.com/en-us/magazine/jj863136.aspx Daha fazla ikna etmeye ihtiyacınız varsa, optimizasyonların eklenebileceği bu yeni makaleyi okuyun geçici olmadan çalışmasını engelleyen: msdn.microsoft.com/en-us/magazine/jj883956.aspx Özetle, şu an için geçici olmadan "sizin için" işe yarayabilir ", ancak uygun kodu yazıp her ikisini de kullanma şansı yok uçucu veya uçucu / yazma yöntemleri.
user2685937

1

lockYeterlidir. MS dil spesifikasyonunun (3.0) kendisi §8.12'de bu kesin senaryodan bahsedilmeksizin şunlardan bahsedilmeden bahseder volatile:

Daha iyi bir yaklaşım, özel bir statik nesneyi kilitleyerek statik verilere erişimi senkronize etmektir. Örneğin:

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) {
          ...
        }
    }
}

Jon Skeet makalesinde ( yoda.arachsys.com/csharp/singleton.html ) bu durumda uygun bellek bariyeri için uçucuya ihtiyaç olduğunu söylüyor. Marc, buna yorum yapabilir misin?
Konstantin

Ah, iki kez kontrol edilen kilidi fark etmemiştim; basitçe: bunu yapma ;-p
Marc Gravell

Aslında çift kontrol edilmiş kilidin performans açısından iyi bir şey olduğunu düşünüyorum. Ayrıca bir alanı kilit içinden erişilirken geçici hale getirmek gerekirse, çift kontrol kilidi diğer kilitlerden çok daha kötü değildir ...
Konstantin

Ama Jon'un bahsettiği ayrı sınıf yaklaşımı kadar iyi mi?
Marc Gravell

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.