Uçucu anahtar kelime C # içinde ne zaman kullanılmalıdır?


299

Herkes C # volatile anahtar kelime için iyi bir açıklama sağlayabilir? Hangi problemleri çözer, hangilerini çözmez? Hangi durumlarda kilitleme kullanımını kurtarır?


6
Neden kilitleme kullanımından tasarruf etmek istiyorsunuz? Korumasız kilitler programınıza birkaç nanosaniye ekler. Gerçekten birkaç nanosaniye göze alamaz mısınız?
Eric Lippert

Yanıtlar:


273

Bunu cevaplamak için Eric Lippert'den daha iyi bir kişi olduğunu sanmıyorum (orijinalde vurgu):

C # 'da "uçucu" sadece "derleyici ve titreşimin herhangi bir kod yeniden sıralaması yapmadığından veya bu değişken üzerinde önbellek optimizasyonlarını kaydetmediğinden emin olun" anlamına gelir. Ayrıca, "diğer işlemcileri durdurup ana belleği önbellekleriyle senkronize etmeleri anlamına gelse bile, işlemcilere en son değeri okuduğumdan emin olmak için yapmaları gereken her şeyi yapmalarını söyleyin" anlamına gelir.

Aslında, bu son parça bir yalan. Uçucu okuma ve yazmaların gerçek anlambilimi, burada özetlediğimden çok daha karmaşıktır; aslında onlar her işlemci ne yaptığını durur aslında garanti yok ana bellekten / 'e ve güncellemeleri önbelleğe. Aksine, okuma ve yazma işlemlerinden önce ve sonra belleğe nasıl erişildiğine dair daha zayıf garantiler sağlarlar . Yeni bir iş parçacığı oluşturma, bir kilit girme veya Kilitli yöntem ailesinden birini kullanma gibi belirli işlemler, siparişin gözlemlenmesi konusunda daha güçlü garantiler sağlar. Daha fazla ayrıntı istiyorsanız, C # 4.0 spesifikasyonunun 3.10 ve 10.5.3 bölümlerini okuyun.

Açıkçası, seni hiç dalgalı bir alan yapmaktan caydırıyorum . Uçucu alanlar, çılgınca bir şey yaptığınızın bir işaretidir: bir kilit yerine koymadan aynı değeri iki farklı iş parçacığında okumaya ve yazmaya çalışıyorsunuz. Kilitler, kilidin içinde okunan veya değiştirilen belleğin tutarlı olarak gözlemlendiğini, kilitler ise bir seferde belirli bir belleğe yalnızca bir iş parçacığının eriştiğini garanti eder. Bir kilidin çok yavaş olduğu durumların sayısı çok azdır ve tam bellek modelinin çok büyük olduğunu anlamadığınız için kodu yanlış alma olasılığınız çok yüksektir. Kilitli işlemlerin en önemsiz kullanımları dışında düşük kilitli kod yazmaya çalışmıyorum. "Uçucu" kullanımını gerçek uzmanlara bırakıyorum.

Daha fazla okuma için bakınız:


29
Yapabilseydim bunu aşağıya indirirdim. Orada birçok ilginç bilgi var, ama sorusuna gerçekten cevap vermiyor. Kilitleme ile ilgili olarak uçucu anahtar kelimenin kullanımını soruyor. Oldukça uzun bir süre (2.0 RT'den önce), field örneği yapıcıda herhangi bir başlatma kodu içeriyorsa, statik bir alan iş parçacığını düzgün bir şekilde güvenli hale getirmek için geçici anahtar kelimenin kullanılması gerekiyordu (bkz. AndrewTek'in cevabı). Hala üretim ortamlarında 1.1 RT kodu bir sürü var ve onu korumak devs neden bu anahtar kelime orada ve kaldırmak güvenli olup olmadığını bilmek gerekir.
Paul Easter

3
O gerçeği @PaulEaster olabilir (genellikle tekil desende) doulbe-kontrol kilitleme için kullanılacak bu ortalama değil should . .NET bellek modeline güvenmek muhtemelen kötü bir uygulamadır - bunun yerine ECMA modeline güvenmelisiniz. Örneğin, farklı bir modeli olan bir gün mono'ya bağlantı kurmak isteyebilirsiniz. Ayrıca farklı donanım mimarilerinin bazı şeyleri değiştirebileceğini de anlıyorum. Daha fazla bilgi için bkz . Stackoverflow.com/a/7230679/67824 . Daha iyi singleton alternatifleri için (tüm .NET sürümleri için) bakınız: csharpindepth.com/articles/general/singleton.aspx
Ohad Schneider

6
Başka bir deyişle, sorunun doğru cevabı şöyledir: Kodunuz 2.0 çalışma zamanında veya daha sonra çalışıyorsa, geçici anahtar kelimeye neredeyse hiç gerek yoktur ve gereksiz yere kullanıldığında yarardan çok zarar verir. Ancak çalışma zamanının önceki sürümlerinde, statik alanlarda düzgün çift kontrol kilitleme için IS gereklidir.
Paul Easter

3
bu kilitler ve uçucu değişkenler aşağıdaki anlamda karşılıklı olarak münhasır mıdır: bazı değişkenlerin etrafında kilit kullandıysam, bu değişkeni artık geçici olarak bildirmeye gerek yoktur?
giorgim

4
@Giorgi evet - garanti edilen bellek engelleri volatilekilit sayesinde orada olacak
Ohad Schneider

54

Geçici anahtar kelimenin ne yaptığı hakkında biraz daha teknik bilgi almak istiyorsanız, aşağıdaki programı göz önünde bulundurun (DevStudio 2005 kullanıyorum):

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

Standart optimize (bırakma) derleyici ayarlarını kullanarak, derleyici aşağıdaki derleyiciyi (IA32) oluşturur:

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              

Çıktıya bakıldığında derleyici, j değişkeninin değerini saklamak için ecx yazmacı kullanmaya karar verdi. Uçucu olmayan döngü için (ilk) derleyici eax kaydına i atamıştır. Yeterince açıksözlü. Yine de birkaç ilginç bit var - lea ebx, [ebx] komutu etkili bir şekilde çok baytlı bir nop talimatıdır, böylece döngü 16 baytlık hizalanmış bir bellek adresine atlar. Diğeri, inc eax komutunu kullanmak yerine döngü sayacını artırmak için edx kullanımıdır. Add reg, reg komutunun, birkaç IA32 çekirdeğinde, inc komutuna kıyasla daha düşük gecikme süresi vardır, ancak hiçbir zaman daha yüksek gecikme süresi yoktur.

Şimdi uçucu döngü sayacı olan döngü için. Sayaç [esp] dizininde saklanır ve geçici anahtar sözcük derleyiciye değerin her zaman belleğe okunması / belleğe yazılması ve hiçbir zaman bir kayıt defterine atanmaması gerektiğini bildirir. Derleyici, sayaç değerini güncellerken üç ayrı adım (yük eax, inc eax, eax kaydet) olarak bir yük / artış / depolama yapmayacak kadar ileri gider, bunun yerine bellek doğrudan tek bir komutta değiştirilir (bir ek bellek reg). Kodun oluşturulma şekli, döngü sayacının değerinin her zaman tek bir CPU çekirdeği bağlamında güncel olmasını sağlar. Veriler üzerinde hiçbir işlem bozulmaya veya veri kaybına yol açamaz (bu nedenle, yük sırasında / inc / store kullanılmaz, çünkü inc sırasındaki değer değişebilir ve böylece mağazada kaybolabilir). Kesmeler ancak mevcut talimat tamamlandıktan sonra servis edilebildiğinden,

Sisteme ikinci bir CPU eklediğinizde, geçici anahtar kelime aynı anda başka bir CPU tarafından güncellenen verilere karşı koruma sağlamaz. Yukarıdaki örnekte, potansiyel bir bozulma elde etmek için verilerin atanmamış olması gerekir. Uçucu anahtar kelime, veriler atomik olarak işlenemezse olası bozulmayı engellemez; örneğin, döngü sayacı uzun (64 bit) türdeyse, değeri güncellemek için iki adet 32 ​​bit işlem gerekir. bir kesinti meydana gelebilir ve verileri değiştirebilir.

Bu nedenle, uçucu anahtar sözcük yalnızca işlemler her zaman atomik olacak şekilde yerel kayıtların boyutundan küçük veya ona eşit olan hizalanmış veriler için iyidir.

Uçucu anahtar kelime, IO'nun sürekli değişeceği ancak bellek eşlemeli UART cihazı gibi sabit bir adresin bulunduğu IO işlemleriyle kullanılmak üzere tasarlandı ve derleyici adresten okunan ilk değeri tekrar kullanmaya devam etmemelidir.

Büyük verileri işliyorsanız veya birden fazla CPU'nuz varsa, veri erişimini düzgün bir şekilde işlemek için daha yüksek bir düzey (OS) kilitleme sistemine ihtiyacınız olacaktır.


Bu C ++ ancak ilke C # için geçerlidir.
Skizz

6
Eric Lippert, C ++ 'daki uçucunun yalnızca derleyicinin bazı optimizasyonlar yapmasını engellediğini yazarken, C # uçucuda ayrıca en son değerin okunmasını sağlamak için diğer çekirdekler / işlemciler arasında bazı iletişim de vardır.
Peter Huber

44

.NET 1.1 kullanıyorsanız, çift denetimli kilitleme yaparken geçici anahtar sözcük gerekir. Neden? .NET 2.0'dan önce, aşağıdaki senaryo ikinci bir iş parçacığının boş olmayan, ancak tam olarak oluşturulmamış bir nesneye erişmesine neden olabilir:

  1. İş parçacığı 1, bir değişkenin boş olup olmadığını sorar. //if(this.foo == null)
  2. İş parçacığı 1, değişkenin null olduğunu belirler, bu nedenle bir kilit girer. //lock(this.bar)
  3. İş parçacığı 1, TEKRAR değişkenin boş olup olmadığını sorar. //if(this.foo == null)
  4. İş parçacığı 1 yine de değişkenin null olduğunu belirler, bu nedenle yapıcıyı çağırır ve değeri değişkene atar. //this.foo = yeni Foo ();

.NET 2.0'dan önce, this.foo'ya, kurucunun çalışması bitmeden yeni Foo örneği atanabilir. Bu durumda, ikinci bir iş parçacığı gelebilir (iş parçacığının Foo'nun yapıcısına yaptığı çağrı sırasında) ve aşağıdakilerle karşılaşabilirsiniz:

  1. İş parçacığı 2 değişkenin boş olup olmadığını sorar. //if(this.foo == null)
  2. İş parçacığı 2, değişkenin null DEĞİL olduğunu belirler, bu nedenle kullanmaya çalışır. //this.foo.MakeFoo ()

.NET 2.0'dan önce, this.foo'yu bu soruna geçici bir çözüm bulmak için geçici olarak bildirebilirsiniz. .NET 2.0'dan bu yana, çift denetimli kilitlemeyi gerçekleştirmek için artık geçici anahtar sözcüğü kullanmanıza gerek yoktur.

Wikipedia'nın Çift Kontrollü Kilitleme hakkında iyi bir makalesi var ve bu konuya kısaca değiniyor: http://en.wikipedia.org/wiki/Double-checked_locking


2
Bu tam olarak eski bir kodda gördüğüm şey ve bunu merak ediyordum. bu yüzden daha derin bir araştırma başlattım. Teşekkürler!
Peter Porfy

1
Konu 2 nasıl değer atayacağını anlamıyorum foo? Diş 1 kilitlenmez this.barve bu nedenle yalnızca diş 1, foo'yu zamanında verilen bir noktada başlatabilir mi? Yani, kilit tekrar bırakıldıktan sonra değeri kontrol edersiniz, yine de diş 1'den yeni bir değere sahip olmalıdır.
gilmishal

25

Derleyici bazen bir alanı optimize eder ve saklamak için bir kayıt kullanır. İş parçacığı 1 alana yazma yaparsa ve başka bir iş parçacığı bu alana erişirse, güncelleştirme bir kayıtta (bellekte değil) depolandığından, 2. iş parçacığı eski verileri alır.

Uçucu anahtar kelimeyi "Bu değeri hafızada saklamanızı istiyorum" derleyicisine söyleyerek düşünebilirsiniz. Bu, 2. iş parçacığının en son değeri almasını garanti eder.


21

Gönderen MSDN : Uçucu değiştirici genellikle serialize erişimine kilit deyimi kullanmadan birden çok iş parçacığı tarafından erişilen bir alan için kullanılır. Uçucu değiştiriciyi kullanmak, bir iş parçacığının başka bir iş parçacığı tarafından yazılan en güncel değeri almasını sağlar.


13

CLR, talimatları optimize etmeyi sever, bu nedenle koddaki bir alana eriştiğinizde, alanın her zaman geçerli değerine erişmeyebilir (yığından vb. Olabilir). Bir alanı işaretlemek, alanın volatilegeçerli değerine talimatla erişilmesini sağlar. Bu değer, programınızdaki eşzamanlı bir iş parçacığı veya işletim sisteminde çalışan başka bir kod tarafından değiştirilebiliyorsa (kilitlemesiz bir senaryoda).

Açıkçası bazı optimizasyonları kaybedersiniz, ancak kodu daha basit tutar.


3

Joydip Kanjilal'ın bu makalesini çok yardımcı buldum !

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

Referans için burada bırakacağım


0

Derleyici bazen en iyi duruma getirmek için koddaki ifadelerin sırasını değiştirir. Normalde bu tek iş parçacıklı ortamlarda bir sorun değildir, ancak çok iş parçacıklı ortamlarda bir sorun olabilir. Aşağıdaki örneğe bakın:

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

T1 ve t2'yi çalıştırırsanız, sonuç olarak hiçbir çıktı veya "Değer: 10" beklemezsiniz. Derleyici t1 fonksiyonu içindeki hattı değiştirebilir. T2 yürütülürse, _flag değeri 5 olabilir, ancak _value 0 olabilir. Bu nedenle beklenen mantık bozulabilir.

Bunu düzeltmek için alana uygulayabileceğiniz geçici bir anahtar kelime kullanabilirsiniz . Bu ifade, derleyici optimizasyonlarını devre dışı bırakır, böylece kodunuzda doğru sırayı zorlayabilirsiniz.

private static volatile int _flag = 0;

Bazı derleyici optimizasyonlarını devre dışı bıraktığından, performansı gerçekten olumsuz etkileyeceğinden, uçucuyu yalnızca gerçekten ihtiyacınız varsa kullanmalısınız . Ayrıca tüm .NET dilleri tarafından desteklenmez (Visual Basic desteklemez), bu nedenle dil birlikte çalışabilirliğini engeller.


2
Örneğin çok kötü. Programcı, ilk önce t1 kodunun yazıldığı gerçeğine dayanarak, t2 görevindeki _flag değeri hakkında hiçbir beklentide bulunmamalıdır. Önce yazıldı! = Önce idam edildi. Derleyicinin t1'deki bu iki satırı değiştirmesi önemli değildir. Derleyici bu ifadeleri değiştirmemiş olsa bile, başka daldaki Console.WriteLne öğeniz _flag üzerindeki geçici anahtar kelimeyi kullanarak bile çalışmaya devam edebilir.
Jakotheshadows

@jakotheshadows, haklısın, cevabımı düzenledim. Ana fikrim t1 ve
t2'yi

0

Tüm bunları özetlemek gerekirse, sorunun doğru cevabı şöyledir: Kodunuz 2.0 çalışma zamanında veya daha sonra çalışıyorsa, geçici anahtar kelimeye neredeyse hiç gerek yoktur ve gereksiz yere kullanıldığında yarardan daha fazla zarar verir. IE Hiç kullanma. ANCAK çalışma zamanının önceki sürümlerinde, statik alanlarda düzgün çift kontrol kilitleme için gereklidir. Sınıfı statik sınıf başlatma koduna sahip olan özellikle statik alanlar.


-4

birden çok evre bir değişkene erişebilir. En son güncelleme değişken üzerinde olacak

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.