Uçucuya Karşı Kilitli ve Kilitli


671

Diyelim ki bir sınıfın public int counterbirden çok iş parçacığı tarafından erişilen bir alanı var. Bu intyalnızca artırılır veya azaltılır.

Bu alanı arttırmak için hangi yaklaşım kullanılmalı ve neden?

  • lock(this.locker) this.counter++;,
  • Interlocked.Increment(ref this.counter);,
  • Erişim değiştiricisini olarak counterdeğiştirin public volatile.

Şimdi keşfettiğime göre volatile, birçok lockifadeyi ve kullanımını kaldırdım Interlocked. Ama bunu yapmamak için bir neden var mı?


İş parçacığı C # başvurusunda okuyun . Sorunuzun giriş ve çıkışlarını kapsar. Üçünün her birinin farklı amaçları ve yan etkileri vardır.
spoulson

1
basit-talk.com/blogs/2012/01/24/… dizilerde volitable kullanımını görebilirsiniz, ben tam olarak anlamıyorum, ama bunun ne yaptığı için daha iyi bir referans.
eran otzap

50
Bu, "yağmurlama sisteminin hiçbir zaman etkinleştirilmediğini keşfettim, bu yüzden onu kaldıracağım ve duman alarmlarıyla değiştireceğim" demek gibidir. Bunu yapmamanın nedeni, inanılmaz derecede tehlikeli olması ve size neredeyse hiç fayda sağlamamasıdır . Kodu değiştirmek için zamanınız varsa , daha az çok iş parçacıklı hale getirmenin bir yolunu bulun ! Çok iş parçacıklı kodu daha tehlikeli ve kolayca kırılabilecek bir yol bulamayın!
Eric Lippert

1
Evim sprinkler hem sahiptir ve duman alarmları. Bir iş parçacığındaki bir sayacı artırıp başka bir iş parçacığında okurken, hem bir kilide (ya da Birbirine Kilitli) hem de geçici anahtar kelimeye ihtiyacınız var gibi görünüyor . Hakikat?
yoyo

2
@yoyo Hayır, ikisine de ihtiyacınız yok.
David Schwartz

Yanıtlar:


859

En kötü (aslında çalışmaz)

Erişim değiştiricisini olarak counterdeğiştirpublic volatile

Diğer insanların belirttiği gibi, bu tek başına aslında güvenli değildir. Bunun anlamı, volatilebirden fazla CPU üzerinde çalışan birden çok iş parçacığının verileri önbelleğe alıp talimatlarını yeniden sıralayabilmesidir.

Eğer öyleyse değil volatile , ve CPU A değerini artırır, daha sonra CPU B aslında sorunlara neden olabilir daha sonra bir süre, kadar bu arttırılmış değer göremeyebilirsiniz.

Öyleyse volatile, bu sadece iki CPU'nun aynı anda aynı verileri görmesini sağlar. Onlardan kaçınmaya çalıştığınız sorun olan okuma ve yazma işlemlerini bırakmasını engellemez.

En iyi ikinci:

lock(this.locker) this.counter++;

Bunu yapmak güvenlidir ( lockeriştiğiniz her yere hatırlamanız koşuluyla this.counter). Diğer tüm evreler tarafından korunan diğer kodların yürütülmesini engeller locker. Kilitleri kullanarak, yukarıdaki gibi çoklu CPU yeniden sıralama sorunlarını önler, bu harika.

Sorun, kilitleme yavaş ve eğer lockergerçekten ilgili olmayan başka bir yerde yeniden kullanırsanız, o zaman diğer ipliklerinizi hiçbir sebeple bloke edebilirsiniz.

En iyi

Interlocked.Increment(ref this.counter);

Bu, güvenli bir şekilde kesilemeyen 'tek vuruşta' okuma, arttırma ve yazma işlemlerini yaptığı için güvenlidir. Bu nedenle, diğer kodları etkilemez ve başka bir yerde de kilitlemeyi hatırlamanız gerekmez. Ayrıca çok hızlıdır (MSDN'in söylediği gibi, modern CPU'larda, bu genellikle tam anlamıyla tek bir CPU talimatıdır).

Ancak, diğer CPU'ların bir şeyleri yeniden sıralaması veya geçici olarak artışı ile birleştirmeniz gerekiyorsa emin değilim.

InterlockedNotes:

  1. İNTERLOK EDİLMİŞ YÖNTEMLER HERHANGİ BİR ÇEKİRDEK VEYA İŞLEMCİ ÜZERİNDE TAMAMEN GÜVENLİDİR.
  2. Kilitli yöntemler, yürüttükleri talimatların etrafına tam bir çit uygular, bu nedenle yeniden sıralama gerçekleşmez.
  3. Birbirine kenetlenmiş yöntemlerin uçucu bir alana erişime ihtiyacı yoktur ve hatta desteklemezler , çünkü uçucu verilen alandaki işlemlerin etrafına yarım bir çit yerleştirilir ve kenetlenmiş tam çit kullanılır.

Dipnot: Uçucu olan aslında ne işe yarar.

As volatileiçin bu kadar ne mulithread bu tür konuları engellemez? İyi bir örnek, biri her zaman bir değişkene (diyelim queueLength) ve diğeri her zaman aynı değişkenten okuyan iki iş parçacığına sahip olduğunuzu söylemektir .

queueLengthUçucu değilse , A iş parçacığı beş kez yazabilir, ancak B iş parçacığı bu yazma işlemlerini geciktirildiğini (hatta potansiyel olarak yanlış sırada) görebilir.

Bir çözüm kilitlemek olacaktır, ancak bu durumda uçucu da kullanabilirsiniz. Bu, B iş parçacığının her zaman A iş parçacığının yazdığı en güncel şeyi görmesini sağlar. Not Ancak bu mantık o sadece yazdığınız asla hiç okumamış yazarlar ve okuyucular varsa, işler ve şey sen yazma atomik değeri ise. Tek bir okuma-değiştirme-yazma işlemi yaptığınızda, Kilitli işlemlere gitmeniz veya bir Kilit kullanmanız gerekir.


29
"Tamamen emin değilim ... eğer uçucuyu artımla birleştirmeniz gerekiyorsa." Ref tarafından bir uçucuyu geçemediğimiz için AFAIK birleştirilemezler. Bu arada harika bir cevap.
Hosam Aly

41
Çok teşekkürler! "Uçucu olanın aslında ne işe yaradığı" konusundaki dipnotunuz aradığım ve uçucu olanı nasıl kullanmak istediğimi doğrulayan şeydi.
Jacques Bosch

7
Diğer bir deyişle, bir değişken uçucu olarak bildirilirse, derleyici kodunuzun her karşı karşıya gelişinde değişkenin değerinin aynı kalmayacağını (yani geçici) olmayacağını varsayar. Yani: (m_Var) {} gibi bir döngüde ve m_Var başka bir iş parçacığında false olarak ayarlanırsa, derleyici daha önce m_Var değeri ile yüklenmiş bir kayıtta zaten ne olduğunu kontrol etmez, ancak m_Var'dan değeri okur tekrar. Ancak, uçucu bildirilmemesi döngünün sonsuza kadar devam etmesine neden olacağı anlamına gelmez - uçucu belirtmek yalnızca m_Var başka bir iş parçacığında false değerine ayarlandığında bunu yapmayacağını garanti eder.
Zach Testere

35
@Zach Saw: C ++ için bellek modeli altında, uçucu onu nasıl tanımladığınızdır (temel olarak cihaz eşlemeli bellek için yararlıdır ve başka bir şey değil). CLR için bellek modelinin altında (bu soru C # olarak etiketlenmiştir), uçucu o depolama konumuna okuma ve yazma işlemlerinin etrafına bellek engelleri ekleyecektir. Bellek engelleri (ve bazı montaj talimatlarının özel kilitli varyasyonları), işlemciye bir şeyleri yeniden sıralamamasını söylüyorsunuz ve bunlar oldukça önemli ...
Orion Edwards

19
@ZachSaw: C # içindeki geçici bir alan, C # derleyicisinin ve jit derleyicisinin değeri önbelleğe alacak belirli optimizasyonlar yapmasını engeller. Ayrıca, birden çok iş parçacığında hangi sıranın okuduğu ve yazıldığının gözlenebileceği konusunda bazı garantiler verir. Bir uygulama detayı olarak, okuma ve yazma işlemlerinde bellek engelleri koyarak bunu yapabilir. Garantili kesin anlambilim şartnamede açıklanmıştır; şartname olmadığını not değil bir garanti tutarlı bir sipariş tüm uçucu yazma ve tarafından gözlemlenecektir okur bütün parçacıkları.
Eric Lippert

147

EDIT: Yorumlarda belirtildiği gibi, bugünlerde açıkçası iyi olduğu tek bir değişkenInterlocked durumlarda kullanmak için mutluyum . Daha karmaşık hale geldiğinde, yine de kilitlemeye döneceğim ...

Artırmanız volatilegerektiğinde kullanmak yardımcı olmaz - çünkü okuma ve yazma ayrı talimatlardır. Başka bir evre, okuduktan sonra ancak tekrar yazmadan önce değeri değiştirebilir.

Şahsen neredeyse her zaman kilitliyorum - açık bir şekilde volatilite veya İnterloktan daha doğru bir şekilde almak daha kolaydır . Benim endişe duyduğum kadarıyla, kilitsiz çoklu iş parçacığı gerçek iş parçacığı uzmanları içindir, ki ben değilim. Joe Duffy ve ekibi, inşa edeceğim bir şey kadar kilitlemeden şeyleri paralelleştirecek güzel kütüphaneler inşa ederse, bu muhteşem ve bunu bir kalp atışı içinde kullanacağım - ancak diş açarken kendimi yapmaya çalışıyorum basit olsun.


16
Artık kilitsiz kodlamayı unutmamı sağlayan +1.
Xaqron

5
kilitsiz kodlar kesinlikle (FSB) otobüs ya da interCPU düzeyinde, herhangi bir aşamada kilitlendiklerinden kesinlikle kilitlenmezler, yine de ödemeniz gereken bir ceza vardır. Ancak bu düşük seviyelerde kilitleme, kilidin oluştuğu yerdeki bant genişliğini doyurmadığınız sürece genellikle daha hızlıdır.
Zach Saw

2
İnterlocked ile ilgili yanlış bir şey yok, tam olarak aradığınız şey ve tam bir kilitten daha hızlı ()
Jaap

5
@Jaap: Evet, bu gün ben ederim gerçek bir tek sayaç için kenetlenmiş kullanın. Değişkenlere yönelik çoklu kilitsiz güncellemeler arasındaki etkileşimleri çözmeye çalışırken uğraşmak istemiyorum .
Jon Skeet

6
@ZachSaw: İkinci yorumunuz kilitli işlemlerin bir aşamada "kilitlendiğini" söylüyor; "kilit" terimi genellikle bir görevin sınırsız bir süre boyunca kaynağın münhasır kontrolünü koruyabildiğini; kilitsiz programlamanın birincil avantajı, sahip olunan görevin yoldan çıkarılmasının bir sonucu olarak kaynakların kullanılamaz hale gelme tehlikesini ortadan kaldırmasıdır. Kilitli sınıf tarafından kullanılan veri yolu senkronizasyonu sadece "genellikle daha hızlı" değildir - çoğu sistemde sınırlı bir en kötü durum zamanı vardır, oysa kilitler yoktur.
supercat

44

" volatile" yerine geçmiyor Interlocked.Increment! Sadece değişkenin önbelleğe alınmamasını, doğrudan kullanılmasını sağlar.

Bir değişkeni artırmak aslında üç işlem gerektirir:

  1. okumak
  2. artım
  3. yazmak

Interlocked.Increment üç parçayı da tek bir atomik işlem olarak gerçekleştirir.


4
Başka bir deyişle, Kilitli değişiklikler tam çitle çevrilidir ve atomiktir. Uçucu elemanlar sadece kısmen çitlerle çevrilidir ve bu nedenle diş açmaya karşı garantili değildir.
JoeGeeky

1
Aslında volatileyok değil emin değişken önbelleğe olun. Sadece nasıl önbelleklenebileceğine dair kısıtlamalar getirir. Örneğin, donanımda tutarlı oldukları için CPU'nun L2 önbelleğinde hala önbelleğe alınabilir. Yine de tercih edilebilir. Yazmalar yine de önbelleğe gönderilebilir, vb. (Bu da Zach'in elde ettiği şeydi.)
David Schwartz

42

Kilit ya da kilitli artış aradığınız şeydir.

Uçucu kesinlikle peşinde olduğunuz şey değildir - sadece geçerli kod yolu derleyicinin bellekten bir okumayı optimize etmesine izin verse bile, derleyiciye değişkeni her zaman değişen olarak ele almasını söyler.

Örneğin

while (m_Var)
{ }

m_Var başka bir iş parçacığında false değerine ayarlanırsa ancak uçucu olarak bildirilmezse, derleyici, CPU kaydına (örneğin EAX olduğu için m_Var'ın bellek konumuna başka bir okuma yapmak yerine (bu önbelleklenebilir - biz bilmiyoruz ve umursamıyoruz ve bu x86 / x64'ün önbellek tutarlılığının noktası). Talimatların yeniden sıralanmasından bahseden diğerleri tarafından daha önce gönderilen tüm yayınlar, x86 / x64 mimarilerini anlamadıklarını gösteriyor. Uçucu değil'Yeniden sıralamayı önler' diyerek önceki gönderilerin ima ettiği gibi okuma / yazma engelleri çıkarır. Aslında, yine MESI protokolü sayesinde, gerçek sonuçların fiziksel belleğe bırakılmış olmasına veya sadece yerel CPU'nun önbelleğinde bulunmasına bakılmaksızın, okuduğumuz sonucun CPU'lar arasında her zaman aynı olduğu garanti edilir. Bunun ayrıntılarına çok fazla girmeyeceğim, ancak bu yanlış giderse Intel / AMD'nin muhtemelen bir işlemci geri çağrısı yapacağından emin olabilirsiniz! Bu aynı zamanda sıra dışı yürütme vb. İle ilgilenmemiz gerekmediği anlamına gelir. Sonuçlar her zaman sırayla emekli olur - aksi takdirde doldurulur!

Kilitli Artım ile, işlemcinin dışarı çıkması, verilen adresten değeri alması, daha sonra artırması ve geri yazması gerekir - tüm bunlar, başka hiçbir işlemcinin değiştiremeyeceğinden emin olmak için tüm önbellek satırının (sahiplik xadd) özel mülkiyetine sahipken Değeri.

Uçucu ile, yine de sadece 1 talimat alacaksınız (JIT'in olması gerektiği gibi verimli olduğu varsayılarak) - inc dword ptr [m_Var]. Bununla birlikte, işlemci (cpuA), kilitli sürümle yaptığı her şeyi yaparken önbellek hattının münhasır sahipliğini istemez. Tahmin edebileceğiniz gibi, bu, diğer işlemcilerin cpuA tarafından okunduktan sonra m_Var'a güncellenmiş bir değer yazabileceği anlamına gelir. Şimdi değeri iki kez artırmak yerine, sadece bir kez elde edersiniz.

Umarım bu sorunu giderir.

Daha fazla bilgi için, bkz. 'Çok İş Parçacıklı Uygulamalarda Düşük Kilit Tekniklerinin Etkisini Anlama' - http://msdn.microsoft.com/en-au/magazine/cc163715.aspx

ps Ne bu çok geç cevap istedi? Tüm cevaplar çok açık bir şekilde yanlış (özellikle bir cevap olarak işaretlenmiş) açıklamalarında sadece bunu okuyan herkes için temizlemek zorunda kaldı. silkiyor

pps Hedefin IA64 değil x86 / x64 olduğunu varsayıyorum (farklı bir bellek modeli var). Microsoft'un ECMA spesifikasyonlarının en güçlü olanı yerine en zayıf bellek modelini belirtmesi nedeniyle berbat edildiğini unutmayın (platformlar arasında tutarlı olması için en güçlü bellek modeline karşı her zaman daha iyidir - aksi takdirde x86'da 24-7 çalışacak kod Intel, IA64 için benzer şekilde güçlü bir bellek modeli uygulamasına rağmen x64, IA64 üzerinde hiç çalışmayabilir) - Microsoft bunu kabul etti - http://blogs.msdn.com/b/cbrumme/archive/2003/05/17/51445.aspx .


3
İlginç. Buna başvurabilir misiniz? Bunu memnuniyetle oylarım, ancak okuduğum kaynaklarla tutarlı yüksek oy alan bir cevaptan 3 yıl sonra bazı agresif bir dille yayınlamak biraz daha somut bir kanıt gerektirecek.
Steven Evers

Referans vermek istediğiniz bölüme işaret edebiliyorsanız, bir yerden bir şeyler kazmaktan memnuniyet duyarım (x86 / x64 satıcılarının ticari sırlarını vermiş olduğumdan şüpheliyim, bu yüzden wiki, Intel'den kolayca erişilebilir olmalıdır PRM'ler (programcının referans kılavuzları), MSFT blogları, MSDN veya benzeri bir şey) ...
Zach Saw

2
Neden herkes CPU'nun önbelleğe alınmasını önlemek isteyecek? Önbellek tutarlılığını gerçekleştirmek için ayrılmış tüm gayrimenkul (kesinlikle ihmal edilemez) tamamen boşa harcanır ... Grafik kartı, PCI cihazı vb.Gibi önbellek tutarlılığı gerektirmedikçe, ayarlamazsınız. yazılacak bir önbellek satırı.
Zach Saw

4
Evet, söylediğin her şey% 100 değilse en az% 99. Bu site (çoğunlukla) iş yerinde gelişme telaşında oldukça yararlıdır, ancak maalesef (oy) oylarına karşılık gelen cevapların doğruluğu yoktur. Yani temelde stackoverflow'da okuyucuların popüler anlayışının gerçekte ne olduğu hakkında bir fikir edinebilirsiniz. Bazen en iyi cevaplar sadece saf anlamsızdır - tür mitler. Ve ne yazık ki bu sorunu çözerken okumaya rastlayan insanlara üreyen şey budur. Yine de anlaşılabilir, kimse her şeyi bilemez.
user1416420

1
@BenVoigt Devam edip .NET'in çalıştığı tüm mimariler hakkında cevap verebilirdim, ancak bu birkaç sayfa alır ve kesinlikle SO için uygun değildir. Donanım mem-modelinin altında en yaygın olarak kullanılan .NET'e dayanarak insanları eğitmek, keyfi olanlardan daha iyidir. Ve 'her yerde' yorumlarımla, insanların önbelleği yıkama / geçersiz kılma varsayımlarında yaptıkları hataları düzeltiyordum. Hangi donanımı belirtmeden temel donanım hakkında varsayımlar yaptılar.
Zach

16

Kilitli fonksiyonlar kilitlenmez. Atomiktirler, yani artış sırasında bir bağlamsal geçiş olasılığı olmadan tamamlanabilirler. Yani kilitlenme veya bekleme şansı yok.

Bunu her zaman bir kilit ve artışa tercih etmelisiniz diyebilirim.

Bir iş parçacığında başka bir iş parçacığında okunması gerekiyorsa ve en iyi duruma getiricinin bir değişken üzerindeki işlemleri yeniden sıralamamasını istiyorsanız (en iyi duruma getiricinin bilmediği başka bir iş parçacığında işler olduğu için) Uçucu kullanışlıdır. Nasıl artırdığınıza göre dikey bir seçimdir.

Kilitsiz kod hakkında daha fazla bilgi edinmek ve bunu yazmaya yaklaşmanın doğru yolunu öğrenmek istiyorsanız, bu gerçekten iyi bir makale

http://www.ddj.com/hpc-high-performance-computing/210604448


11

lock (...) çalışır, ancak bir iş parçacığını engelleyebilir ve başka bir kod aynı kilitleri uyumsuz bir şekilde kullanıyorsa kilitlenmeye neden olabilir.

İnterlocked. * Bunu yapmanın doğru yoludur ... modern CPU'lar bunu ilkel olarak desteklediğinden çok daha az ek yük.

kendi başına uçuculuk doğru değildir. Değiştirilmiş bir değeri alıp geri yazmaya çalışan bir iş parçacığı yine de bunu yapan başka bir iş parçacığı ile çakışabilir.


8

Teorinin nasıl çalıştığını görmek için bazı testler yaptım: kennethxu.blogspot.com/2009/05/interlocked-vs-monitor-performance.html . Testim CompareExchnage'a daha fazla odaklandı, ancak Artış sonucu benzer. Kilitli çoklu işlemci ortamında daha hızlı gerekli değildir. İşte 2 yıllık 16 CPU sunucusunda Increment için test sonucu. Testin, gerçek dünyada tipik olan artıştan sonra güvenli okumayı da içerdiğini unutmayın.

D:\>InterlockVsMonitor.exe 16
Using 16 threads:
          InterlockAtomic.RunIncrement         (ns):   8355 Average,   8302 Minimal,   8409 Maxmial
    MonitorVolatileAtomic.RunIncrement         (ns):   7077 Average,   6843 Minimal,   7243 Maxmial

D:\>InterlockVsMonitor.exe 4
Using 4 threads:
          InterlockAtomic.RunIncrement         (ns):   4319 Average,   4319 Minimal,   4321 Maxmial
    MonitorVolatileAtomic.RunIncrement         (ns):    933 Average,    802 Minimal,   1018 Maxmial

Test ettiğiniz kod örneği çok önemsiz - gerçekten bu şekilde test etmek pek mantıklı değil! En iyisi, farklı yöntemlerin gerçekte ne yaptığını anlamak ve sahip olduğunuz kullanım senaryosuna göre uygun olanı kullanmak olacaktır.
Zach

@Zach, burada nasıl bir sayaç güvenli bir şekilde bir sayaç artırmak senaryosu hakkında oldu. Aklınızda başka hangi kullanım senaryosu vardı veya nasıl test edersiniz? Yorum için teşekkürler BTW.
Kenneth Xu

Asıl nokta, yapay bir test. Herhangi bir gerçek dünya senaryosunda sık sık aynı yere girmeyeceksiniz. Eğer öyleyse, o zaman FSB tarafından tıkanırsınız (sunucu kutularınızda gösterildiği gibi). Her neyse, blogunuzdaki cevabıma bakın.
Zach Saw

2
Tekrar bakýyorum. Gerçek darboğaz FSB'de ise, monitör uygulamasında aynı darboğaz gözlenmelidir. Gerçek fark, Interlocked'in yüksek bekleme sayımı ile gerçek bir sorun haline gelen meşgul bekleme ve yeniden deneme yapmasıdır. En azından umarım yorumum, Interlocked'un her zaman sayım için doğru seçim olmadığına dikkat çeker. Halkın alternatiflere bakması bunu iyi açıkladı. Uzun bir toplayıcıya ihtiyacınız var gee.cs.oswego.edu/dl/jsr166/dist/jsr166edocs/jsr166e/…
Kenneth Xu

8

3

Ben fark arasındaki diğer cevaplar belirtilen eklemek istiyorum volatile, Interlockedve lock:

Geçici anahtar kelime, aşağıdaki türdeki alanlara uygulanabilir :

  • Referans türleri.
  • İşaretçi türleri (güvenli olmayan bir bağlamda). İşaretçinin kendisi değişken olabilse de, işaret ettiği nesnenin yapamayacağını unutmayın. Başka bir deyişle, bir "işaretçi" nin "geçici" olduğunu bildiremezsiniz.
  • Gibi basit türleri sbyte, byte, short, ushort, int, uint, char, float, ve bool.
  • Aşağıdaki temel türlerinden biri olan bir numaralama türü: byte, sbyte, short, ushort, intya da uint.
  • Referans türleri olduğu bilinen genel tip parametreleri.
  • IntPtrve UIntPtr.

Diğer tür de dahil olmak üzere, doubleve long, okur ve bu türde alanlara yazma atomik olması garanti edilemez çünkü "uçucu" işaretli edilemez. Bu tür alanlara çok iş parçacıklı erişimi korumak için, Interlockedsınıf üyelerini kullanın veya lockifadeyi kullanarak erişimi koruyun .

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.