Kilitsiz çoklu diş açma gerçek diş açma uzmanları içindir


87

Bir içinden okuyordu cevap olduğunu Jon Skeet bir soruya ve o bu sözü o verdi:

Benim ilgilendiğim kadarıyla, kilitsiz çoklu iş parçacığı, benim biri olmadığım gerçek diş açma uzmanları içindir.

Bunu ilk kez duymuyorum, ancak kilitsiz çoklu iş parçacığı kodunun nasıl yazılacağını öğrenmekle ilgileniyorsanız, bunu gerçekte nasıl yaptığınız hakkında konuşan çok az insan buluyorum.

Öyleyse sorum, iş parçacığı vb. Hakkında öğrenebileceğiniz her şeyi öğrenmenin yanı sıra, özellikle kilitsiz çok iş parçacıklı kod yazmayı ve bazı iyi kaynakların neler olduğunu öğrenmeye nereden başlıyorsunuz?

Şerefe


Gcc, linux ve X86 / X68 platformlarını kullanıyorum. Kilitsiz, neredeyse hepsi ses çıkardığı kadar zor değildir! Gcc atomic yerleşiklerin bilgi üzerinde bellek engelleri vardır, ancak bu gerçek hayatta önemli değildir. Önemli olan hafızanın atomik olarak değiştirilmiş olmasıdır. Sadece, başka bir iş parçacığı bir değişiklik gördüğünde önemli olmayan "kilitsiz" veri yapıları tasarladığınızda sarsılır. Tek bağlantılı listeler, atlama listeleri, karma tablolar, ücretsiz listeler vb. Hepsi kilit gerektirmeden yapmak oldukça kolaydır. Kilitsiz her şey için değil. Bu, belirli durumlar için doğru olan başka bir araçtır.
johnnycrash


Bir kaynak önerisi olarak kapatmak için oy vermek veya ne istediğinizi netleştirmemek.
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功

Yanıtlar:


101

Mevcut "kilitsiz" uygulamalar çoğu zaman aynı modeli izler:

  • bir durumu okuyun ve bir kopyasını alın *
  • kopyayı değiştir *
  • kilitli bir işlem yapmak
  • başarısız olursa tekrar dene

(* isteğe bağlı: veri yapısına / algoritmaya bağlıdır)

Son parça ürkütücü bir şekilde spinlock'a benzer. Aslında, temel bir spinlock'tur . :)
Bu konuda @nobugz ile aynı fikirdeyim: Kilitsiz çoklu iş parçacığı işleminde kullanılan birbirine bağlı işlemlerin maliyetinde, gerçekleştirmesi gereken önbellek ve bellek tutarlılığı görevleri hakimdir .

Bununla birlikte, "kilitsiz" bir veri yapısıyla elde ettiğiniz şey, "kilitlerinizin" çok ince taneli olmasıdır. . Bu, iki eşzamanlı iş parçacığının aynı "kilide" (bellek konumu) erişme şansını azaltır.

Çoğu zaman işin püf noktası, adanmış kilitlerinizin olmamasıdır - bunun yerine, örneğin bir dizideki tüm öğeleri veya bağlantılı bir listedeki tüm düğümleri bir "döndürme kilidi" olarak ele alırsınız. Son okumanızdan bu yana güncelleme yoksa okur, değiştirir ve güncellemeye çalışırsınız. Varsa, yeniden deneyin.
Bu, ek bellek veya kaynak gereksinimleri gerektirmeden "kilitlemenizi" (üzgünüm, kilitlemesiz :) çok ince taneli hale getirir.
Daha ince taneli hale getirilmesi bekleme olasılığını azaltır. Ek kaynak gereksinimleri getirmeden olabildiğince ince ayar yapmak kulağa harika geliyor, değil mi?

Ancak eğlencenin çoğu, doğru yükleme / mağaza siparişi vermekten kaynaklanabilir .
Kişinin sezgilerinin aksine, CPU'lar bellek okuma / yazma işlemlerini yeniden sıralamakta özgürdür - bu arada çok akıllıdırlar: Bunu tek bir iş parçacığından gözlemlemekte zorlanacaksınız. Bununla birlikte, birden çok çekirdekte çoklu iş parçacığı oluşturmaya başladığınızda sorunlarla karşılaşacaksınız. Sezgileriniz çökecek: bir talimat kodunuzda daha önce olduğu için, aslında daha önce gerçekleşeceği anlamına gelmez. CPU'lar talimatları sıra dışı işleyebilirler ve bunu özellikle bellek erişimiyle ilgili talimatlar için, ana bellek gecikmesini gizlemek ve önbelleklerinden daha iyi yararlanmak için yapmaktan hoşlanırlar.

Şimdi, sezgiye karşı, bir kod dizisinin "yukarıdan aşağıya" akmadığı, bunun yerine hiç bir sekans yokmuş gibi çalıştığı ve "şeytanın oyun alanı" olarak adlandırılabileceği kesindir. Hangi yükleme / mağaza siparişlerinin gerçekleşeceği konusunda kesin bir cevap vermenin mümkün olmadığına inanıyorum. Bunun yerine, kişi her zaman mayıslar , mights ve teneke kutular açısından konuşur ve en kötüsüne hazırlanır. "Ah, CPU olabilir bu noktada, burada bir bellek bariyer koymak en iyisidir, böylece o yazma önce gelip bu okuma yeniden düzenlemek."

Bu olasılıklar ve güçlükler bile CPU mimarileri arasında farklılık gösterebildiği için meseleler karmaşıktır . Bu olabilir , örneğin, yani bir şey durum olmaz garanti bir mimaride meydana gelebilecek diğerine.


Doğru "kilitsiz" çoklu iş parçacığı elde etmek için, bellek modellerini anlamanız gerekir.
Bellek modelini ve garantileri doğru almak önemsiz değildir, ancak bu hikayedeMFENCE gösterildiği gibi , Intel ve AMD, JVM geliştiricileri arasında bazı karışıklıklara neden olmak için belgelerde bazı düzeltmeler yaptı . Görünüşe göre, geliştiricilerin başından beri güvendikleri dokümantasyon ilk etapta o kadar kesin değildi.

Örtük bellek bariyer içinde .NET sonucu Kilitler, çoğu zaman olduğunu ... örneğin bkz (bunları kullanarak güvenli nedenle bu Joe Duffy - Brad Abrams - Vance Morrison büyüklüğünün tembel başlatma, kilitler, uçucuların ve bellek engeller. :) (O sayfadaki bağlantıları takip ettiğinizden emin olun.)

Ek bir bonus olarak, bir yan görevde .NET bellek modeliyle tanışacaksınız . :)

Ayrıca Vance Morrison'dan bir "eski ama goldie" var: Çok İş Parçacıklı Uygulamalar Hakkında Her Dev'in Bilmesi Gerekenler .

... ve elbette, @Eric'in bahsettiği gibi, Joe Duffy konu hakkında kesin bir okuma.

İyi bir STM, ince ayarlı kilitlemeye olabildiğince yaklaşabilir ve muhtemelen el yapımı bir uygulamaya yakın veya buna eşit bir performans sağlayacaktır. Bunlardan biri olan STM.NET gelen DevLabs projelerin MS.

Yalnızca .NET fanatiği değilseniz, Doug Lea JSR-166'da harika işler çıkardı .
Cliff Click , kilit şeritlemeye dayanmayan - Java ve .NET eşzamanlı karma tablolarının yaptığı gibi - hash tablolarına ilginç bir yaklaşım getiriyor ve 750 CPU'ya kadar iyi ölçekleniyor gibi görünüyor.

Linux bölgesine girmekten korkmuyorsanız, aşağıdaki makale mevcut bellek mimarilerinin iç yapıları ve önbellek hattı paylaşımının performansı nasıl bozabileceği hakkında daha fazla bilgi sağlar: Her programcının bellek hakkında bilmesi gerekenler .

@Ben, MPI hakkında birçok yorum yaptı: MPI'nin bazı alanlarda parlayabileceğine içtenlikle katılıyorum. MPI tabanlı bir çözüm, akıllı olmaya çalışan yarı pişmiş bir kilitleme uygulamasından daha kolay akıl yürütebilir, daha kolay uygulanabilir ve daha az hataya açık olabilir. (Bununla birlikte - öznel olarak - STM tabanlı bir çözüm için de geçerlidir.) Pek çok başarılı örneğin öne sürdüğü gibi, örneğin Erlang'da düzgün dağıtılmış bir uygulamayı doğru bir şekilde yazmanın ışık yılı daha kolay olduğuna da bahse girerim .

Ancak MPI, tek, çok çekirdekli bir sistemde çalıştırıldığında kendi maliyetlerine ve kendi sorunlarına sahiptir . Örneğin, Erlang'da, süreç planlaması ve mesaj kuyruklarının senkronizasyonu etrafında çözülmesi gereken sorunlar vardır .
Ayrıca, özünde, MPI sistemleri genellikle "hafif süreçler" için bir tür işbirliğine dayalı N: M çizelgeleme uygular. Bu, örneğin, hafif süreçler arasında kaçınılmaz bir bağlam değişikliği olduğu anlamına gelir. Bunun bir "klasik bağlam anahtarı" değil, çoğunlukla bir kullanıcı alanı işlemi olduğu doğrudur ve hızlı yapılabilir - ancak içtenlikle , kilitli bir işlemin gerektirdiği 20-200 döngü altına getirilebileceğinden içtenlikle şüpheliyim . Kullanıcı modu bağlam değiştirme kesinlikle daha yavaştırIntel McRT kitaplığında bile. N: Hafif süreçlerle M planlama yeni değil. LWP'ler uzun süre Solaris'teydi. Terk edildiler. NT'de lifler vardı. Artık çoğunlukla bir kalıntılar. NetBSD'de "aktivasyonlar" vardı. Terk edildiler. Linux, N: M iş parçacığı konusunda kendi görüşüne sahipti. Şimdiye kadar biraz ölmüş gibi görünüyor.
Zaman zaman yeni yarışmacılar vardır: örneğin Intel'den McRT veya en son Microsoft'tan ConCRT ile birlikte Kullanıcı Modu Planlama . En düşük seviyede, bir N: M MPI programlayıcının yaptığını yaparlar. Erlang - veya herhangi bir MPI sistemi - yeni UMS'den yararlanarak SMP sistemlerinde büyük fayda sağlayabilir .

Sanırım OP'nin sorusu, herhangi bir çözüme yönelik / aleyhindeki öznel argümanlar ve esası ile ilgili değil, ancak buna cevap vermek zorunda kalırsam, sanırım bu göreve bağlıdır: tek bir sistem ile birçok çekirdek , ya düşük kilitleme / "kilit-serbest" teknikleri veya STM yukarıdaki kırışıklıklar gidermesinden bile, performans açısından en iyi sonuçlar verecektir ve muhtemelen epeyce bir MPI çözümüdür her zaman döverdi örneğin Erlang'da.
Tek bir sistem üzerinde çalışan orta derecede daha karmaşık herhangi bir şey oluşturmak için, belki klasik kaba taneli kilitlemeyi veya performans çok önemliyse bir STM'yi seçerdim.
Dağıtılmış bir sistem kurmak için, bir MPI sistemi muhtemelen doğal bir seçim olacaktır.
için de MPI uygulamaları olduğunu unutmayın.NET de (etkin görünmese de).


1
Bu cevap çok sayıda iyi bilgiye sahip olsa da, kilitsiz algoritmaların ve veri yapılarının aslında sadece çok ince taneli spinlock'lardan oluşan bir koleksiyon olduğu şeklindeki başlık fikri yanlıştır. Genellikle kilitsiz yapılarda yeniden deneme döngüleri görseniz de, davranış çok farklıdır: kilitler (spinlock'lar dahil) özel olarak bir miktar kaynak alır ve diğer iş parçacıkları tutulduğunda ilerleme kaydedemez. Bu anlamda "yeniden deneme", yalnızca özel kaynağın serbest bırakılmasını beklemektir.
BeeOnRope

1
Kilitsiz algoritmalar ise özel bir kaynak elde etmek için CAS veya diğer atom talimatlarını kullanmaz, bunun yerine bazı işlemleri tamamlamak için kullanılır. Başarısız olurlarsa, bunun nedeni başka bir iş parçacığı ile geçici olarak ince taneli bir yarıştır ve bu durumda diğer iş parçacığı ilerleme kaydetmiştir (işlemini tamamlamıştır). Bir iş parçacığı sonsuza kadar şüpheli ise, diğer tüm iş parçacıkları hala ilerleme kaydedebilir. Bu hem nitelik hem de performans açısından özel kilitlerden çok farklıdır. "Yeniden deneme" sayısı, yoğun çekişmeler altında bile çoğu CAS döngüsü için genellikle çok düşüktür ...
BeeOnRope

1
... ancak bu elbette iyi bir ölçeklendirme anlamına gelmez: tek bir bellek konumu için çekişme, SMP makinelerinde her zaman oldukça yavaş olacaktır, yalnızca çekirdekler arası yuvalar arası gecikmeler nedeniyle, CAS arızalarının sayısı az olsa bile düşük.
BeeOnRope

1
@AndrasVass - Sanırım bu aynı zamanda "iyi" ve "kötü" kilitsiz koda da bağlı. Kesinlikle herkes bir yapı yazabilir ve ona kilitsiz diyebilir, oysa o gerçekten sadece kullanıcı modu spinlock kullanır ve tanıma bile uymaz. Ayrıca, ilgilenen okuyucuları , kilit tabanlı ve kilitsiz algoritmaların çeşitli kategorilerine resmi bir şekilde bakan Herlihy ve Shavit'in bu makalesini incelemelerini tavsiye ederim . Herlihy tarafından bu konudaki her şeyin okunması da önerilir.
BeeOnRope

1
@AndrasVass - Katılmıyorum. Klasik kilitsiz yapıların (listeler, kuyruklar, eşzamanlı haritalar, vb.) Çoğu, paylaşılan değişken yapılar için bile dönmemiştir ve örneğin Java'da aynı olan pratik mevcut uygulamalar aynı modeli takip etmektedir ( yerel olarak derlenmiş C veya C ++ 'da neyin mevcut olduğuna aşina ve çöp toplama olmadığı için orada daha zor). Belki siz ve ben farklı bir eğirme tanımına sahibiz: Kilitsiz "dönen" şeylerde bulduğunuz "CAS yeniden denemesini" düşünmüyorum. IMO "eğirme", sıcak beklemeyi ifade eder.
BeeOnRope

28

Joe Duffy'nin kitabı:

http://www.bluebytesoftware.com/books/winconc/winconc_book_resources.html

Ayrıca bu konularla ilgili bir blog da yazıyor.

Düşük kilitli programları doğru bir şekilde elde etmenin püf noktası , sizin özel donanım, işletim sistemi ve çalıştırma ortamı kombinasyonunuzdaki bellek modelinin kurallarının tam olarak ne olduğunu derin bir düzeyde anlamaktır .

Şahsen ben InterlockedIncrement'in ötesinde doğru düşük kilitli programlamayı yapacak kadar akıllı değilim, ama harikaysanız, bunun için gidin. Kodda çok sayıda belge bıraktığınızdan emin olun, böylece sizin kadar akıllı olmayan insanlar yanlışlıkla bellek modeli değişmezlerinizden birini kırmaz ve bulunması imkansız bir hata ortaya koymaz.


40
Yani her ikisi de eğer Eric Lippert'ın ve Jon Skeet kilidi serbest programlama akıllı kendilerinden daha insanlar için tek olduğunu düşünüyorum, o zaman ben naçizane hemen fikrinden çığlık uzağa çalışacaktır. ;-)
dodgy_coder

20

Bu günlerde "kilitsiz diş çekme" diye bir şey yok. Geçen yüzyılın sonunda, bilgisayar donanımının yavaş ve pahalı olduğu zaman, akademi ve benzerleri için ilginç bir oyun alanıydı. Dekker'in algoritması her zaman en sevdiğim şeydi, modern donanım onu ​​otlattı. Artık çalışmıyor.

İki gelişme bunu sona erdirdi: RAM ve CPU hızı arasındaki artan eşitsizlik. Ve çip üreticilerinin bir çip üzerine birden fazla CPU çekirdeği koyma yeteneği.

RAM hızı sorunu, çip tasarımcılarının CPU yongasına bir arabellek koymasını gerektiriyordu. Tampon, kodu ve verileri depolar ve CPU çekirdeği tarafından hızla erişilebilir. Ve RAM'den / RAM'e çok daha yavaş bir hızda okunabilir ve yazılabilir. Bu arabelleğe CPU önbelleği denir, çoğu CPU'da en az iki tane bulunur. 1. seviye önbellek küçük ve hızlıdır, 2. seviye büyük ve daha yavaştır. CPU 1. seviye önbellekten verileri ve talimatları okuyabildiği sürece hızlı çalışacaktır. Önbellek kaçırma gerçekten pahalıdır, veriler 1. önbellekte değilse CPU'yu 10 döngü, 2. önbellekte değilse ve okunması gerekiyorsa 200 döngü kadar uykuya sokar. VERİ DEPOSU.

Her CPU çekirdeğinin kendi önbelleği vardır, kendi RAM "görünümünü" saklarlar. CPU veri yazdığında, yazma önbelleğe yapılır ve daha sonra yavaşça RAM'e boşaltılır. Kaçınılmaz, her çekirdek artık RAM içeriğinin farklı bir görünümüne sahip olacaktır. Başka bir deyişle, bir CPU, RAM yazma döngüsü tamamlanana ve CPU kendi görünümünü yenileyene kadar başka bir CPU'nun ne yazdığını bilmez .

Bu, diş açma ile büyük ölçüde uyumsuzdur. Başka bir iş parçacığı tarafından yazılan verileri okumanız gerektiğinde, başka bir iş parçacığının durumunun ne olduğunu her zaman gerçekten önemsersiniz. Bunu sağlamak için, sözde bir bellek engeli programlamanız gerekir. Tüm CPU önbelleklerinin tutarlı bir durumda olmasını ve RAM'in güncel bir görünümüne sahip olmasını sağlayan düşük seviyeli bir CPU ilkelidir. Bekleyen tüm yazma işlemleri RAM'e boşaltılmalı, ardından önbelleklerin yenilenmesi gerekir.

Bu .NET'te mevcuttur, Thread.MemoryBarrier () yöntemi bir tane uygular. Bunun, kilit ifadesinin yaptığı işin% 90'ı (ve yürütme süresinin% 95'i) olduğu göz önüne alındığında, .NET'in size verdiği araçlardan kaçınarak ve kendi araçlarınızı uygulamaya çalışarak ileride değilsiniz.


2
@ Davy8: kompozisyon hala zorlaştırıyor. İki kilitsiz hash-tablom varsa ve bir tüketici olarak ikisine de erişirsem, bu bir bütün olarak devlet tutarlılığını garanti etmeyecektir. Bugün gelebileceğiniz en yakın nokta, iki erişimi örneğin tek bir atomicblokta koyabileceğiniz STM'lerdir . Sonuç olarak, kilitsiz yapıları kullanmak çoğu durumda aynı derecede zor olabilir.
Andras Vass

5
Yanılıyor olabilirim, ancak önbellek tutarlılığının nasıl çalıştığını yanlış açıkladığınızı düşünüyorum. Çoğu modern çok çekirdekli işlemcinin tutarlı önbellekleri vardır, bu da önbellek donanımının tüm işlemlerin aynı RAM içeriği görünümüne sahip olmasını sağlaması anlamına gelir - tüm karşılık gelen "yazma" çağrıları tamamlanana kadar "okuma" çağrılarını bloke ederek. Thread.MemoryBarrier () dokümantasyonu ( msdn.microsoft.com/en-us/library/… ), önbellek davranışı hakkında hiçbir şey söylemez - bu, işlemcinin okuma ve yazma işlemlerini yeniden düzenlemesini engelleyen bir yönergedir.
Brooks Moses

7
"Bu günlerde" kilitsiz diş çekme "diye bir şey yok." Bunu Erlang ve Haskell programcılarına anlatın.
Juliet

4
@HansPassant: "Bu günlerde 'kilitsiz diş açma' diye bir şey yok". F #, Erlang, Haskell, Cilk, OCaml, Microsoft'un Görev Paralel Kitaplığı (TPL) ve Intel'in Dişli Yapı Taşları (TBB), kilitsiz çok iş parçacıklı programlamayı teşvik eder. Bu günlerde üretim kodunda nadiren kilit kullanıyorum.
JD

6
@HansPassant: "bir bellek engeli. Tüm CPU önbelleklerinin tutarlı bir durumda olmasını ve güncel bir RAM görünümüne sahip olmasını sağlayan düşük seviyeli bir CPU ilkelidir. Bekleyen tüm yazmalar, RAM'e boşaltılmalıdır. önbelleklerin daha sonra yenilenmesi gerekir ". Bu bağlamdaki bir bellek engeli, bellek talimatlarının (yükler ve depolar) derleyici veya CPU tarafından yeniden sıralanmasını engeller. CPU önbelleklerinin tutarlılığıyla hiçbir ilgisi yok.
JD


0

Çoklu iş parçacığı oluşturma söz konusu olduğunda, tam olarak ne yaptığınızı bilmeniz gerekir. Demek istediğim, çok iş parçacıklı bir ortamda çalışırken oluşabilecek tüm olası senaryoları / durumları araştırmak. Kilitsiz çoklu okuma, dahil ettiğimiz bir kütüphane veya sınıf değil, iş parçacıkları yolculuğumuz sırasında kazandığımız bir bilgi / deneyim.


Kilitsiz iş parçacığı semantiği sağlayan çok sayıda kitaplık vardır. STM özellikle ilgi çekicidir ve çevresinde oldukça fazla sayıda uygulama vardır.
Marcelo Cantos

Bunun her iki tarafını da görüyorum. Kilitsiz bir kitaplıktan etkili performans elde etmek, bellek modelleri hakkında derin bilgi gerektirir. Ancak bu bilgiye sahip olmayan bir programcı, doğruluk avantajlarından yine de yararlanabilir.
Ben Voigt

0

NET'te kilitsiz diş açma zor olsa da, kilit kullanırken tam olarak neyin kilitlenmesi gerektiğini inceleyerek ve kilitli bölümü en aza indirerek önemli iyileştirmeler yapabilirsiniz ... bu aynı zamanda kilit ayrıntı düzeyini en aza indirmek olarak da bilinir .

Örnek olarak, bir koleksiyon iş parçacığını güvenli hale getirmeniz gerektiğini söyleyin. Her öğe üzerinde CPU yoğun bir görev gerçekleştiriyorsa, koleksiyon üzerinde yinelenen bir yöntemin etrafına körü körüne kilit atmayın. Sen belki sadece toplama sığ kopyasını oluşturarak etrafında bir kilit koymak gerekir. Kopya üzerinde yinelemek, kilit olmadan çalışabilir. Elbette bu, büyük ölçüde kodunuzun özelliklerine bağlıdır, ancak bu yaklaşımla bir kilit konvoyu sorununu çözebildim .

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.