Android için SMP yardımcı programı

Android 3.0 ve sonraki platform sürümleri, Android 3.0 ve sonraki sürümleri çok işlemcili mimarilere sahip olması gerekir. Bu dokümanda, proje yöneticisinin C, C++ ve Java dillerinde simetrik çok işlemcili sistemler için çok iş parçacıklı kod yazılırken ortaya çıkabilir (bundan sonra programlama dili olarak (bundan sonra yalnızca “Java” olarak anılacaktır) . Tam bir değil, Android uygulama geliştiricileri için bir rehber olarak tasarlanmıştır. gerektiğini bileceksiniz.

Giriş

SMP, "Ssimetrik Çok İşlemci"nin kısaltmasıdır. Bir tasarımı anlatır. ana belleğe erişimi paylaşan iki veya daha fazla özdeş CPU çekirdeğidir. Bitiş birkaç yıl önce tüm Android cihazlar UP (Tek İşlemci) modundaydı.

Hepsi olmasa da çoğu Android cihazın her zaman birden fazla CPU'su vardır. Geçmişte bu uygulamalardan yalnızca biri uygulamaları çalıştırmak için kullanılırken diğerleri cihazın çeşitli parçalarını yönetirdi donanım (örneğin, radyo). CPU'lar farklı mimarilere sahip olabilir ve bu programların her biri ile iletişim kurmak için ana belleği diğer.

Günümüzde satılan Android cihazların çoğu SMP tasarımlarına dayanır. işini yazılım geliştiriciler için biraz daha karmaşık hale getiriyor. Yarış koşulları tek işlemcili olmayan sistemlerde görünür sorunlara neden ancak ileti dizilerinizden iki veya daha fazlası olduğunda düzenli olarak başarısız olabilir. farklı çekirdeklerde aynı anda çalışıyor. Dahası, kod farklı bir üzerinde çalıştırıldığında hatalara daha çok veya daha az açıktır. veya aynı mimarinin farklı uygulamalarında bile bahsedeceğim. x86'da kapsamlı bir şekilde test edilmiş kod, ARM'da kötü bir şekilde bozulabilir. Kod, daha modern bir derleyiciyle yeniden derlendiğinde başarısız olmaya başlayabilir.

Bu belgenin geri kalanında bunun nedenini ve ne yapmanız gerektiğini kontrol edin.

Bellek tutarlılığı modelleri: SMP'ler neden biraz farklıdır?

Bu, karmaşık bir konunun yüksek hızlı ve parlak bir özetidir. Bazı alanlarda eksiksiz olmalıdır, ancak hiçbiri yanıltıcı veya yanlış olmamalıdır. Siz sonraki bölümde göreceğiniz gibi, buradaki ayrıntılar genellikle önemli değildir.

İlgili belgenin sonundaki Daha fazla bilgi konusuyla ilgili daha kapsamlı yaklaşımlara yönelik işaretçiler sunar.

Bellek tutarlılığı modelleri veya çoğu zaman sadece “bellek modelleri” programlama dilini veya donanım mimarisini garanti eder hafıza erişimlerinden bahsediyor. Örneğin, A adresine bir değer yazar, ardından B adresine bir değer yazarsanız model, her CPU çekirdeğinin bu yazma işlemlerinin sipariş.

Çoğu programcının alışkın olduğu model sıralı tutarlılık, şu şekilde tanımlanır (Adve & Gharachorloo):

  • Tüm bellek işlemlerinin tek seferde yürütüldüğü görülüyor
  • Tek bir iş parçacığındaki tüm işlemlerin açıklanan sırayla yürütüldüğü görülüyor tarafından işleme konabilir.

Geçici bir süreliğine çok basit bir derleyicimiz veya yorumlayıcımız olduğunu düşünelim. hiçbir sürpriz içermeyen: Bu kelime talimatları tam olarak doğru şekilde yükleyip depolamak için kaynak koddaki sipariş, erişim başına bir talimat. Ayrıca her iş parçacığının kendi işlemcisinde yürütülmesi kolaylığını ortaya koyar.

Bir koda bakıp küçük bir parçadan bazı tutarlı bir CPU mimarisine sahip olması için beklenen sırada yapar. Bir açıklamanın CPU aslında talimatları yeniden sıralıyor, okuma ve yazma işlemlerini geciktiriyor, ancak yine de cihazda çalışan kodun CPU'nun bir şey yaptığını söylemesinin bir yolu yoktur yerine getirebileceğiniz en iyi uygulamalardır. ( bellek eşlenmiş cihaz sürücüsü G/Ç.)

Bu noktaları göstermek için küçük kod snippet’leri, genellikle limus testleri olarak bilinir.

Aşağıda, kodun iki iş parçacığı üzerinde çalıştırıldığı basit bir örnek verilmiştir:

İş parçacığı 1 İş parçacığı 2
A = 3
B = 5
reg0 = B
reg1 = A

Bu ve gelecekteki tüm yarışma örneklerinde bellek konumları büyük harfler (A, B, C) ve CPU kayıtları "reg" ile başlar. Tüm bellek başlangıçta sıfır olması gerekir. Talimatlar yukarıdan aşağıya doğru uygulanır. Burada, 1. ileti dizisi 3 değerini A konumunda, 5 değerini de B konumunda saklar. İş parçacığı 2 B konumundan değeri reg0'a yükler, sonra da A konumunu reg1'e ekleyin. (Tek bir sırayla yazıp okumadığımızı unutmayın başka.)

İş parçacığı 1 ve iş parçacığı 2'nin farklı CPU çekirdeklerinde yürütüleceği varsayılır. Siz bir karara varmayı düşünürken her zaman iş parçacıklı koddur.

Sıralı tutarlılık, her iki iş parçacığı tamamlandıktan sonra yürütülüyorsa kayıtlar aşağıdaki durumlardan birinde olur:

Kaydolanlar Eyaletler
reg0=5, reg1=3 olası (önce ileti dizisi 1 çalıştırıldı)
normal0=0, reg1=0 olası (önce ileti dizisi 2 çalıştırıldı)
reg0=0, reg1=3 mümkün (eş zamanlı yürütme)
reg0=5, reg1=0 hiçbir zaman

Mağazayı A'ya görmeden önce B=5'i gördüğümüz bir duruma gelmek için, okumaların veya yazmaların sıraya göre yapılması gerekir. Bir tutarlı bir makine vardır. Bu yapılamaz.

x86 ve ARM dahil olmak üzere Uni işlemciler normalde sıralı olarak tutarlıdır. İşletim sistemi çekirdeği değişirken iş parçacıkları aralıklı şekilde yürütülüyormuş gibi görünüyor dikkat edin. x86 ve ARM dahil çoğu SMP sistemi, sıralı olarak tutarlı değildir. Örneğin, çalışanlar için depoları belleğe almak için belirli bir donanımla belleğe hemen ulaşmaz ve diğer çekirdekler tarafından görünmez.

Ayrıntılar önemli ölçüde farklılık gösterir. Örneğin, x86 (sıralı olmasa da) yine de reg0 = 5 ve reg1 = 0'ın imkansız kalacağını garanti eder. Mağazalar arabelleğe alınır ancak sıraları korunur. ARM ise bunu yapmaz. Tamponlu mağazaların sırası korunabilir ve mağazalar aynı anda diğer tüm çekirdeklere ulaşmayabilir. Bu farklılıklar, derleme programcıları için önemlidir. Ancak aşağıda göreceğimiz gibi, C, C++ veya Java programcıları, ve bu tür mimari farklılıkları gizleyecek şekilde programlamalıdır.

Şimdiye kadar, gerçekçi olmayan bir şekilde bunun yalnızca bir donanımın yeniden sipariş eden bir öğedir. Aslında derleyici talimatların sırasını da performansı artırır. Örneğimizde, derleyici, daha sonra Thread 2'deki kod, reg0'a ihtiyaç duymadan önce reg1 değerine ihtiyaç duyuyordu ve bu nedenle önce reg1 yazın. Veya önceki bazı kodlar A'yı zaten yüklemiş olabilir ve derleyici A'yı tekrar yüklemek yerine bu değeri yeniden kullanmaya karar verebilir. Her iki durumda da, reg0 ve reg1'e yüklenen yüklemeler yeniden sıralanabilir.

Farklı bellek konumlarına erişimleri yeniden sıralama, donanımda veya derleyicide tek bir iş parçacığının yürütülmesini etkilemediğinden ve performansı önemli ölçüde artırabilir. Göreceğimiz gibi, biraz dikkatli bir şekilde çok iş parçacıklı programların sonuçlarını etkilemesini de önleyebiliriz.

Derleyiciler bellek erişimlerini de yeniden sıralayabildiğinden, bu sorun çok az bilgi bulunur. Tek işlemcili cihazlarda bile, derleyici yüklemeleri yeniden sıralayarak reg0 ile reg1'dir; İleti Dizisi 1 ise yeniden sıralanan talimatlar. Ancak derleyicimiz yeniden sıralama yapmazsa hiçbir zaman gözlemlemeyin. Derleyici olmasa bile çoğu ARM SMP'de büyük olasılıkla, büyük olasılıkla çok büyük bir satın alma işleminden sonra, başarılı yürütmelerin sayısı da vardır. Montajlı programlama yapmıyorsanız spam'ler, genelde çok daha uzun bir süre boyunca yaşanmış devam etmektir.

Veri yarışı içermeyen programlama

Neyse ki genellikle herhangi bir riskin etkisini düşünmenin ele alacağız. Basit kuralları takip ederseniz genelde "sıralı tutarlılık" hariç önceki bölümün tümünü unutmak bölümü. Ancak maalesef diğer komplikasyonlar bu kuralları yanlışlıkla ihlal edebilir.

Modern programlama dilleri, "veri yarışının olmadığı" kavramı teşvik ediyor. programlama stiline odaklanacağız. "Veri yarışları" yapmamaya söz verdiğiniz sürece, ve derleyiciye aksini söyleyen birkaç yapıdan kaçının: derleyici ve donanım, sıralı olarak tutarlı sonuçlar sağlamayı garanti ediyor. Bu iletişim belleğin yeniden sıralanmasından kaçınmış olurlar. Bu demek oluyor ki bu kuralları uygulayarak bellek erişimlerinin kısıtlı bir şekilde yeniden sıralandı. Aslında size sosisin çok lezzetli bir yemek olduğunu söylemek gibi ziyaret etmeme sözü vermediğiniz sürece lezzetli bir yemek yemeye sosis fabrikası. Veri yarışları hafızayla ilgili çirkin gerçeği ortaya çıkarıyor yeniden sıralayın.

"Veri ırkı" nedir?

En az iki iş parçacığı aynı anda eriştiğinde veri yarışı gerçekleşir ve en az biri bu verileri değiştirir. "Normal"e göre verileri" senkronizasyon nesnesi olmayan bir şey yönelik tasarlanmıştır. Yoksayıcılar, koşul değişkenleri, Java Uçucular veya C++ atom nesneleri sıradan veriler değildir ve erişimleri yarışlarına izin veriliyor. Hatta bunlar, diğer sistemlerde veri yarışmasını önlemek için nesneler'i tıklayın.

İki iş parçacığının aynı anda aynı veriye erişip erişmediğini belirlemek için yukarıda belirtilen bellek yeniden sıralama tartışmasını göz ardı edebiliriz ve sıralı tutarlılığı varsayabilirsiniz. Aşağıdaki programda veri yarışı yok A ve B normal boole değişkenleriyse başlangıçta yanlış:

İş parçacığı 1 İş parçacığı 2
if (A) B = true if (B) A = true

İşlemler yeniden sıralanmadığından her iki koşul da yanlış olarak değerlendirilir ve iki değişken de güncellenmez. Bu yüzden bir veri yarışı olamaz. Her biri 100'den az gösterim alan A bölgesinden yüklenirse ne olacağını düşünmenize gerek yoktur. ve şu konumda B üzerinde depola İleti dizisi 1 bir şekilde yeniden sıralandı. Derleyicinin Thread'i yeniden sıralamasına izin verilmiyor 1 değerini "B = true; if (!A) B = false" olarak yeniden yazabilirsiniz. İşte bu, günün ortasında sosis yapmak gibi.

Veri yarışları, tamsayılar ve tamsayılar gibi yerleşik yerleşik türlerde referanslar veya işaretçiler olabilir. Eş zamanlı olarak bir int atanıyor bunu başka bir iş parçacığında okumak açıkça bir veri yarışıdır. Ancak hem C++ standart kitaplık ve Böylece, Java Koleksiyonları kitaplıkları hakkındaki her tür veri yarışına katılıyor. Veri yarışı başlatmama sözü veriyorlar Aynı kapsayıcıya eşzamanlı erişim yoksa otomatik olarak güncellenir. Bir ileti dizisinde set<T> güncelleniyor. aynı anda başka bir dilde okunması, kitaplığın yeni bir Bu nedenle, resmi olmayan bir şekilde "kitaplık düzeyinde veri yarışı" olarak düşünülebilir. Buna karşılık, okuma sırasında bir ileti dizisindeki bir set<T> güncelleniyor ortaya çıkarsa sonuç bir veri yarışına neden olmaz. söz konusu durumda (düşük seviyeli) bir veri yarışı başlatmamayı taahhüt ediyor.

Bir veri yapısındaki farklı alanlara normalde eşzamanlı erişimler veri yarışı başlatamaz. Ancak, bir senaryoda şu kurala göre ele alınır: C veya C++'ta ardışık bit alanı dizileri, bir "bellek konumu" girin. Böyle bir dizideki herhangi bir bit alanına erişme yalnızca, geçerli olan üçüncü tarafların sunduğu bir veri yarışının varlığına işaret eder. Bu, yaygın olarak kullanılan donanımların tek tek bitleri güncellemek için bitişik bitleri okumaya ve yeniden yazmaya gerek yoktur. Java programcılarının da benzer bir endişesi yoktur.

Veri yarışlarından kaçınma

Modern programlama dilleri, belirli hızlarda mekanizmaları kullanıyor. En temel araçlar şunlardır:

Kilitler veya Ses Sistemleri
Müzaklar (C++11 std::mutex veya pthread_mutex_t) veya Java'daki synchronized blokları, belirli web sitelerinin bölümü, kod erişiminin diğer bölümleriyle eş zamanlı olarak çalıştırılmamalıdır aynı verilerdir. Bundan sonra, bu ve benzeri diğer olanaklardan genel olarak bahsedeceğiz "kilit" gibi. Paylaşılan bir kilitlenmeye erişmeden önce sürekli olarak belirli bir kilit edinme ve daha sonra serbest bırakmak, erişim sırasında veri yarışmalarının yaşanmasını önler veri yapısıyla ilgilidir. Aynı zamanda güncellemelerin ve erişimlerin atomik olmasını, yani veri yapısında yapılan diğer güncellemeler ortada çalıştırılabilir. Bunu hak etti önlenmesi için en çok kullanılan araçlardan biridir. Java kullanımı synchronized blok veya C++ lock_guard veya unique_lock, kilitlerin olduğunu unutmayın.
Değişken/atomik değişkenler
Java, eşzamanlı erişimi destekleyen volatile alan sağlar veri yarışı başlatmadan sonuca ulaşabilirsiniz. 2011'den beri C ve C++ desteği Benzer anlamlara sahip atomic değişken ve alan var. Bunlar: kullanımı genellikle kilitlerden daha zordur. Çünkü yalnızca Tek değişkene bağımsız erişim atomiktir. (C++ ürününde bu normal bir şekildedir ve artımlar gibi basit okuma-değiştirme-yazma işlemlerine kadar uzanır. Java Bunun için özel yöntem çağrıları gerekir.) Kilitlerin aksine, volatile veya atomic değişkenleri diğer iş parçacıklarının daha uzun kod dizilerine müdahale etmesini önlemek için doğrudan kullanılabilir.

Buradaki volatile metriğinin oldukça farklı olduğunu unutmayın. anlamları hakkında daha fazla bilgi edinin. C++'ta volatile, verileri engellemez bir çözüm olarak kullansa da, eski kod çoğu zaman atomic nesne. Bu artık önerilmiyor; inç C++, eşzamanlı olarak olabilen değişkenler için atomic<T> kullanın birden çok iş parçacığıyla erişilir. C++ volatile şuna yöneliktir: gibi işlemler yapmanıza olanak tanır.

C/C++ atomic değişkenleri veya Java volatile değişkenleri diğer değişkenlerdeki veri yarışlarını önlemek için kullanılabilir. flag ise atomic<bool> türünde olduğunu açıkladı veya atomic_bool(C/C++) ya da volatile boolean (Java), ve başlangıçta yanlış ise aşağıdaki snippet veri yarışı içermez:

İş parçacığı 1 İş parçacığı 2
A = ...
  flag = true
while (!flag) {}
... = A

Thread 2, flag öğesinin ayarlanmasını beklediğinden İş Parçacığı 2'deki A, İleti Dizisi 1'de A adlı kullanıcıya atama. Dolayısıyla, bir sonraki videoda A flag tarihindeki yarış, bir veri yarışı olarak sayılmaz. Değişken/atomik erişimler "normal bellek erişimleri" değildir.

Belleğin yeniden sıralanmasını önlemek veya gizlemek için uygulama gereklidir gerektiği gibi davrandığından emin olun. Bu durum, normalde değişken/atomik hafıza erişimlerine neden olur daha pahalıya mal olabilir.

Yukarıdaki örnek veri yarışı gerektirmese de Java'da Object.wait() veya C/C++ sürümündeki koşul değişkenleri genellikle sırasında döngüye dahil edilmeyi içermeyen daha iyi bir çözüm pil gücünün çok hızlı tükenmesi anlamına gelir.

Bellek yeniden sıralama görünür hale geldiğinde

Veri yarışı içermeyen programlama, normalde bizi açıkça uğraşma zahmetinden kurtarır. sorunları çözebileceksiniz. Ancak, çeşitli durumlarda görünür hale gelen bir sipariş listesidir:
  1. Programınızda istenmeyen veri yarışına neden olan bir hata varsa derleyici ve donanım dönüşümleri görünür hale gelebilir ve şaşırtıcı olabilir. Örneğin, Önceki örnekte flag değişken, İş Parçacığı 2 ilk başlatılmamış A. Veya derleyici, bu işaretin İş Parçacığı 2'nin döngüsü sırasında değişebilir ve programı
    İş parçacığı 1 İş parçacığı 2
    A = ...
      flag = true
    reg0 = flag; ancak (!reg0) {}
    ... = A
    Hataları ayıkladığınızda, spam'e rağmen döngünün sonsuza kadar devam ettiğini flag doğrudur.
  2. C++, bir araya geldiğinde hiçbir ırk kullanılmasa bile sıralı tutarlılık sağlar. Atomik işlemler açık memory_order_... bağımsız değişkenleri alabilir. Aynı şekilde, java.util.concurrent.atomic paketi daha kısıtlı bir bir dizi benzer özellikle lazySet()’yi kapsıyor. Java programcılar bazen benzer etki için bilinçli veri yarışları kullanırlar. Tüm bunlar geniş çaplı performans iyileştirmeleri sağlar programlamanın karmaşıklığında maliyet{/1}. Bu konuları kısaca tartışıyoruz burada bulabilirsiniz.
  3. Bazı C ve C++ kodları tamamen değil daha eski bir tarzda yazılır volatile dilinin mevcut dil standartlarıyla tutarlı olduğunu değişkenler yerine atomic değişkenleri yerine kullanılıyor ve bellek sıralamalarına göre çit eklenerek veya bariyerler. Bu, erişimle ilgili açık bir akıl yürütmeyi gerektirir ve donanım bellek modellerinin yeniden sıralanması ve anlaşılması. Kodlama stili hâlâ Linux çekirdeğinde kullanılmaya devam ediyor. Reklam metninde kullanılan bazı kaynaklar ve bu kullanımların burada da ele alınmamaktadır.

Alıştırma yap

Bellek tutarlılığı sorunlarında hata ayıklamak çok zor olabilir. Eksikse kilit, atomic veya volatile bildirimi nedenleri eski verileri okumak için bir kod dönüştürmeniz gerekiyorsa, hata ayıklayıcıyla bellek dökümlerini inceleyerek nedenini öğrenebilirsiniz. Bu zamana kadar bir hata ayıklayıcı sorgusu oluştursanız bile, CPU çekirdeklerinin tümü tüm veri kümelerini ve bellek ve CPU kayıtlarının içeriği durumu “imkansız”dır.

C dilinde yapılmaması gerekenler

Burada, yanlış koda ilişkin bazı örnekler ve bu sorunları çözer. Buna geçmeden önce, temel dil kullanımını ele almamız gerekir. özelliğini kullanabilirsiniz.

C/C++ ve "değişken"

C ve C++ volatile bildirimleri çok özel amaçlı bir araçtır. Derleyicinin yeniden sıralamasını veya değişkeni kaldırmasını önlerler erişim. Bu, donanım cihazı kayıtlarına kod erişimi için faydalı olabilir. veya birden fazla konumla bağlantılı setjmp Ancak Java'nın aksine C ve C++ volatile volatile, ileti dizisi iletişimi için tasarlanmamıştır.

C ve C++'ta, volatile erişimi veriler, değişken olmayan verilere erişilerek yeniden sıralanabilir ve üzerinde hiçbir inisiyatif almanın başka yolları da var. Bu nedenle volatile, tek işlemcili cihazlarda bile taşınabilir kodda tutmaktır. C volatile genellikle donanım tarafından yeniden sıralanmasını önleyebilir; bu nedenle, elektronik tablolarda çok iş parçacıklı SMP ortamları bulunur. Bu nedenle C11 ve C++11, atomic nesne. Bunların yerine bunları kullanmalısınız.

Çok sayıda eski C ve C++ kodu, iş parçacığı için volatile öğesini hâlâ kötüye kullanıyor iyi bir iletişimdir. Bu strateji, hedefe uyan verilerde açık çitlerle birlikte kullanılması veya bazı durumlarda önemli olmayan verilerdir. Ancak işe yarayacağı garanti edilmez. doğru şekilde yapılandırmaya çalışın.

Örnekler

Çoğu durumda bir kilit (ör. pthread_mutex_t veya C++11 std::mutex) değil, atomik operasyonu, ancak bu kavramların nasıl daha etkili basit bir iletişim modelidir.

MyThing* gGlobalThing = NULL;  // Wrong!  See below.
void initGlobalThing()    // runs in Thread 1
{
    MyStruct* thing = malloc(sizeof(*thing));
    memset(thing, 0, sizeof(*thing));
    thing->x = 5;
    thing->y = 10;
    /* initialization complete, publish */
    gGlobalThing = thing;
}
void useGlobalThing()    // runs in Thread 2
{
    if (gGlobalThing != NULL) {
        int i = gGlobalThing->x;    // could be 5, 0, or uninitialized data
        ...
    }
}

Buradaki fikir şudur: Bir yapı tahsis eder, alanlarını başlatırız ve en son onu bir genel değişkende depolayarak "yayınlarız". İşte bu noktada diğer ileti dizileri görebilir ancak tamamen başlatıldığı için sorun değil. değil mi?

Sorun, gGlobalThing adresine giden mağazanın gözlemlenmesidir. emin olun. Bu işlem genellikle derleyici veya işleyen, mağazaları gGlobalThing olarak yeniden sıraladı ve thing->x. thing->x kaynağından okuyan başka bir ileti dizisi 5, 0 ve hatta başlatılmamış verilere bakın.

Buradaki temel sorun, gGlobalThing tarihindeki veri yarışı. İleti Dizisi 1, İleti Dizisi 2 sırasında initGlobalThing() öğesini çağırırsa useGlobalThing() aramaları, gGlobalThing şöyle olabilir: okumayı öğreteceğim.

Bu sorun, gGlobalThing öğesinin şu şekilde tanımlanarak düzeltilebilir: atomiktir. C++11'de:

atomic<MyThing*> gGlobalThing(NULL);

Bu, yazma işlemlerinin diğer ileti dizileri tarafından görülebilmesini sağlar sıraya koyun. Ayrıca, gelecekte meydana gelebilecek diğer gerçek zamanlı olarak gerçekleşmeyecek olan, ancak Android donanımı. Örneğin, bu URL'de bir Yalnızca kısmen yazılmış gGlobalThing işaretçi.

Java'da yapılmaması gerekenler

Java diliyle alakalı bazı özelliklerden bahsetmedik, bu nedenle bunlara hızlıca göz atabilirsiniz.

Java, teknik olarak kodun veri yarışı içermeyen olmasını gerektirmez. İşte bu noktada dikkatlice yazılmış ve düzgün çalışan küçük bir Java kodudur. veri yarışına tanık oluyoruz. Ancak bu tür bir kod yazmak, değineceğiz. Bu konuyu aşağıda kısaca ele alacağız. Önemli noktalar Daha da kötüsü, bu kodun anlamını belirten uzmanlar artık doğru olduğundan emin olun. (Bu spesifikasyon, veri ırkı içermeyen girin.)

Şimdilik, Java'nın sağladığı veri yarışı olmayan modele bağlı kalacağız. C ve C++ ile temelde aynı garantilere sahiptir. Burada da dilin sıralı tutarlılığı açıkça gevşeten bazı temel öğeler, özellikle de lazySet() ve weakCompareAndSet() araması java.util.concurrent.atomic içinde. C ve C++ ürününde olduğu gibi, bunları şimdilik yoksayacağız.

Java'nın "synchronized" özelliği "değişken", anahtar kelimeler

"synchronized" anahtar kelimesi, Java dilinin yerleşik kilitleme özelliğini sağlar. mekanizmasıdır. Her nesnenin, aşağıdakileri sağlamak için kullanılabilecek ilişkili bir “denetleyici” vardır. münhasıran erişimdir. İki ileti dizisi "senkronize edilmeye", uygulamasında bir başkası tamamlanıncaya kadar bekler.

Yukarıda belirttiğimiz gibi Java'nın volatile T kelimesi, C++11'lerin atomic<T> öğeleri. Şuna eşzamanlı erişimler: volatile alanlarına izin verilir ve bu alanlar veri yarışına yol açmaz. lazySet() ve diğerleri yoksayılıyor olduğunu varsayalım. Bu durumda Java sanal makinesinin sonucun sıralı olarak tutarlı göründüğünden emin olun.

Özellikle, 1. ileti dizisi bir volatile alanına yazarsa ve iş parçacığı 2 daha sonra aynı alandan okur ve yeni yazılan değerine ayarlanırsa iş parçacığı 2'nin daha önce iş parçacığı 1. Hafıza etkisi açısından, Değişkenlik, monitör sürümüne benzerdir. Değişkenliklerden yararlanarak veri okumak, monitör edinimine benzer.

C++'lar ile atomic arasında göze çarpan bir fark vardır: volatile int x; olarak yazarsak Java'da kullanıldığında x++ ile x = x + 1 aynıdır; o atomik yük gerçekleştirir, sonucu artırır ve ardından atomik bir mağaza. C++'tan farklı olarak, bir bütün olarak artış atomik değildir. Atomik artırma işlemleri bunun yerine java.util.concurrent.atomic.

Örnekler

Aşağıda, monoton bir sayacın basit, yanlış bir uygulamasını görebilirsiniz: (Java teori ve uygulama: Dalgalanmayı yönetme) başlıklı makaleyi okumanızı öneririz.

class Counter {
    private int mValue;
    public int get() {
        return mValue;
    }
    public void incr() {
        mValue++;
    }
}

get() ve incr() öğelerinin birden çok ağdan çağrıldığını varsayın ileti dizileri. Biz de her ileti dizisinin geçerli sayıyı gördüğünden emin olmak istiyoruz. get() çağrıldı. En bariz sorun ise mValue++ aslında üç işlemdir:

  1. reg = mValue
  2. reg = reg + 1
  3. mValue = reg

İki iş parçacığı incr() içinde aynı anda yürütülürse kaybolabilir. Artışı atomik yapmak için incr() "senkronize edildi".

Ancak özellikle SMP'de hâlâ sorunlu. Hâlâ bir veri yarışı var. get(), mValue uygulamasına aynı anda erişebilir. incr(). Java kuralları altında get() çağrısı başka bir koda göre yeniden sıralandığı anlaşılıyor. Örneğin iki farklı sayaçlar eklerse sonuçlar tutarsız görünebilir Çünkü get() aramaları, donanım veya güvenlik nedeniyle yeniden sıraladığımız derleyici olarak da adlandırılır. get() öğesini şu şekilde tanımlayarak sorunu düzeltebiliriz: senkronize edildi. Bu değişiklikle birlikte, kod kesinlikle doğru olacaktır.

Maalesef kilit anlaşmazlığı olasılığını da kullanıma sunduk. performansını düşürebilir. get() değerini senkronize edildiğinde, mValue değerini "değişken" olarak tanımlayabiliriz. ( incr() şu tarihten itibaren synchronize etiketini kullanmaya devam etmelidir: Aksi takdirde, mValue++ tek bir atomik işlem değildir.) Bu işlem, tüm veri yarışlarını da önleyerek sıralı tutarlılık korunur. incr(), hem monitör girişi/çıkışını gerektirdiği için biraz daha yavaş olacaktır ve değişken mağazayla ilişkili genel giderler gibi giderler, get() daha hızlıdır. Bu nedenle, bir çekişme olmasa bile okuması yazarın çok daha fazlaysa bu işe yarar. (Buna ek olarak bkz. AtomicInteger senkronize edilen bloğu kaldırabilirsiniz.)

Burada, önceki C örneklerine benzeyen başka bir örnek verilmiştir:

class MyGoodies {
    public int x, y;
}
class MyClass {
    static MyGoodies sGoodies;
    void initGoodies() {    // runs in thread 1
        MyGoodies goods = new MyGoodies();
        goods.x = 5;
        goods.y = 10;
        sGoodies = goods;
    }
    void useGoodies() {    // runs in thread 2
        if (sGoodies != null) {
            int i = sGoodies.x;    // could be 5 or 0
            ....
        }
    }
}

Bu, C koduyla aynı soruna sahiptir, yani sGoodies tarihinde bir veri yarışı yapacağız. Dolayısıyla, Başlatma işleminden önce sGoodies = goods görülebilir. alanları (goods) içinde görünür. sGoodies öğesini volatile anahtar kelime, sıralı tutarlılık geri yüklenir ve çalışmaya devam eder beklendiği gibi.

Yalnızca sGoodies referansının değişken olduğunu unutmayın. İlgili içeriği oluşturmak için kullanılan için erişimleri kapalıdır. sGoodies, volatile ve bellek sıralaması düzgün şekilde korunduğunda, aynı anda erişilemez. z = sGoodies.x ifadesi, MyClass.sGoodies süreli değişken yükleme gerçekleştirir ardından sGoodies.x değerinde değişken olmayan bir yükleme gerçekleştirildi. Yerel bir mağazanız varsa referans MyGoodies localGoods = sGoodies, sonraki bir z = localGoods.x değişken yükleme gerçekleştirmez.

Java programlamada daha yaygın olarak kullanılan bir deyim, “çift kontrollü kilitleniyor”:

class MyClass {
    private Helper helper = null;
    public Helper getHelper() {
        if (helper == null) {
            synchronized (this) {
                if (helper == null) {
                    helper = new Helper();
                }
            }
        }
        return helper;
    }
}

Buradaki ana fikir, tek bir Helper örneği olmasını istediğimizdir. nesne, MyClass örneğiyle ilişkilendirilmiş. Yalnızca Bu yüzden, özel bir getHelper() aracılığıyla oluşturup iade ediyoruz. işlevini kullanın. İki iş parçacığının örnek oluşturduğu bir rekabetten kaçınmak için senkronize etmek için kullanılır. Ancak, Google'ın reklam ürünlerini Böylece, bu bölümü yalnızca helper şu anda boş.

Bu, helper alanında veri yarışı yapıyor. Bu, Başka bir ileti dizisindeki helper == null ile eş zamanlı olarak ayarlandı.

Bunun nasıl başarısız olabileceğini görmek için aynı kod, C benzeri bir dilde derlenmiş gibi az da olsa yeniden yazılmıştır (Helper’s öğesini temsil edecek birkaç tam sayı alanı ekledim oluşturucu etkinliği):

if (helper == null) {
    synchronized() {
        if (helper == null) {
            newHelper = malloc(sizeof(Helper));
            newHelper->x = 5;
            newHelper->y = 10;
            helper = newHelper;
        }
    }
    return helper;
}

Donanımı veya derleyiciyi önleyen herhangi bir şey yoktur. yeniden sipariş vererek helper yerine bunları x/y alanları için geçerlidir. Başka bir ileti dizisi helper değer null değil, ancak alanları henüz ayarlanmadı ve kullanıma hazır. Daha fazla bilgi ve daha fazla hata modu için Ayrıntılı bilgi için ekte bulunan Kilitleme Bozuk Bildirimi" bağlantısını kullanabilirsiniz. 71 ("Use lazy hakkındaki ilkleştirmeyi makul bir şekilde kullanın"), Josh Bloch'un Effective Java, 2.Sürüm.

Bunu düzeltmenin iki yolu vardır:

  1. Basit olanı yapın ve dış kontrolü silin. Bu sayede hiçbir zaman senkronize edilmiş bir blokun dışındaki helper değerini inceleyin.
  2. helper değişkenliği bildir. Bu küçük bir değişiklikle, işlevi, Java 1.5 ve sonraki sürümlerde doğru şekilde çalışır. (Her bir ekip üyesinin kendinizi bunun doğru olduğuna ikna etmek için bir dakikanızı ayırın.)

Aşağıda, volatile davranışını gösteren başka bir örnek verilmiştir:

class MyClass {
    int data1, data2;
    volatile int vol1, vol2;
    void setValues() {    // runs in Thread 1
        data1 = 1;
        vol1 = 2;
        data2 = 3;
    }
    void useValues() {    // runs in Thread 2
        if (vol1 == 2) {
            int l1 = data1;    // okay
            int l2 = data2;    // wrong
        }
    }
}

useValues() konusuna bakıldığında, Thread 2 henüz vol1 olarak güncellerse, data1 veya data2 henüz ayarlandı. Güncellemeyi gördükten sonra vol1, data1 uygulamasına güvenli bir şekilde erişilebildiğini bilir ve veri yarışı başlatmadan doğru okumasını sağlar. Ancak, data2 hakkında herhangi bir varsayımda bulunamaz çünkü söz konusu mağaza değişeceğini unutmayın.

volatile adlı satıcının yeniden sıralamayı önlemek için kullanılamayacağını unutmayın. birbiriyle yarışan diğer hafıza erişimlerinden. Projenin gidişatı boyunca bir makine bellek çiti talimatı oluşturur. Hastalıkların önlenmesinde yalnızca başka bir iş parçacığı bir sağlayabilir.

Ne yapmalı?

C/C++ ürününde C++11'i tercih edin senkronizasyon sınıfları; örneğin std::mutex. Değilse şunu kullanın: ilgili pthread işlemleri. Bunlar arasında doğru bellek sınırları ve doğru (sırayla tutarlı) aksi belirtilmedikçe) tüm Android platform sürümlerinde etkili bir davranışa sahip. Bunları kullandığınızdan emin olun sağlayabilir. Örneğin, koşul değişkeni bekleme sayısının döndürülmez ve bir döngü içinde görünmelidir.

Veri yapısı devre dışı olmadığı sürece, atom fonksiyonlarını doğrudan kullanmaktan son derece basit olmalı, mesela sayaç gibi. Kilitleniyor ve bir pthread hotspot'unun kilidini açmak için her biri için tek bir atom işlemi gerekir. ve çoğu zaman tek bir önbellekte eksiklik olmadığı durumlarda çekişme, dolayısıyla karşılıklı dışlama çağrılarını atom operasyonları. Önemsiz veri yapıları için kilitsiz tasarımlar için gereklidir üst düzey işlemlerin veri yapısında üst seviyelere ulaşmasını sağlamak için Atomik görünmeleri (sadece atomik parçalarını değil, bir bütün olarak).

Atom işlemleri kullanırsanız, sıralamayı gevşetmek için memory_order... veya lazySet() performans sağlayabilir ancak şu ana kadar aktardığımızdan daha ayrıntılı bir anlayış gerektirir. Mevcut kodun büyük bir kısmı kullanılan bu hatalar bulundu. Mümkünse bunlardan kaçının. Kullanım alanlarınız bir sonraki bölümde belirtilenlerden birine tam olarak uymuyorsa uzman olduğunuzdan veya birine danıştığınızdan emin olmalısınız.

C/C++'ta ileti dizisi iletişimi için volatile kullanmaktan kaçının.

Java'da eşzamanlılık problemlerinin çözümü genellikle uygun bir yardımcı sınıfı kullanarak java.util.concurrent paketi. Kod iyi yazılmış ve test edildi.

Belki de yapabileceğiniz en güvenli şey nesneleri sabit hale getirmektir. Nesneler String ve Integer gibi sınıfların içerdiği verilerin bir kez değiştirilemediğini, bunların Böylece bu nesneler üzerinde veri yarışına yol açabilecek tüm potansiyellerden kaçınmış olursunuz. Kitap Etkili Java, 2. Ed. başlıklı makalede, "Öğe 15: Değişkenliği En Aza İndirme" bölümünde özel talimatlar bulunmaktadır. Not: Java alanlarının “nihai” değerini bildirmenin önemini özellikle (Bloch)'a dokunun.

Bir nesne sabit olsa bile onu başka bir nesneye iletmenin herhangi bir senkronizasyon içermeyen iş parçacığı bir veri yarışıdır. Bu, bazen kabul edilebilir olmalıdır (aşağıya bakın), ancak büyük bir özen gerektirir ve brittle kodu. Bu, performans açısından çok önemli değilse, volatile beyanı. C++'ta, bir işaretçiyi ya da bir sabit nesneye referansta bulunması ve aynı zamanda her veri ırkında olduğu gibi hatalı bir durum. Bu durumda, büyük ihtimalle aralıklı kilitlenmelerle sonuçlanabilir. Örneğin, alıcı iş parçacığı başlatılmamış bir yöntem tablosuyla karşılaşabilir işaretçiyi açmalarını istemeyiz.

Mevcut bir kitaplık sınıfı veya sabit bir sınıf , Java synchronized ifadesi veya C++ Korumak için lock_guard / unique_lock kullanılmalıdır birden fazla iş parçacığı tarafından erişilebilen herhangi bir alana erişir. Karşılıklı dışlamalar ortak bir paydada buluşmasını sağlamakla birlikte, volatile veya atomic. Ancak bu konuda çok dikkatli olmalısınız. ileti dizileri arasındaki etkileşimleri anlayabilir. Bu beyanlar yaygın eşzamanlı programlama hatalarından tasarruf etmenizi sağlar, ancak Derleyicileri ve SMP'yi optimize etmeyle ilgili gizemli hatalardan kaçınabilirsiniz talihsizlikler.

Aşağıdaki durumlardan "yayıncılık" (yani, nesneye referansta bulunurken) başkaları tarafından kullanılabilir iş parçacıklarıdır. C++'ta ya da "veri yarışı yok" tavsiyeleri. Fakat her zaman iyi bir tavsiyedir ve JavaScript kodunuz ise Java güvenlik modelinin önemli olduğu ve güvenilir olmadığı başka bağlamlarda çalıştırılmak kodu, "sızdırılan" o içeriğe erişerek bir veri yarışı başlatabilir. kullanabilirsiniz. Uyarılarımızı göz ardı etmeyi ve tekniklerden bazılarını kullanmayı tercih etmeniz durumunda da göreceğiz. Daha fazla bilgi için (Java'da Güvenli İnşaat Teknikleri) ayrıntılar

Zayıf bellek siparişleri hakkında biraz daha fazla bilgi

C++11 ve sonraki sürümler ardışık düzeni gevşetmek için açık mekanizmalar sağlar tutarlılık garantileri veriyor. Müstehcen memory_order_relaxed, memory_order_acquire (yüklemeler) yalnızca) ve atomik için memory_order_release(yalnızca depolar) bağımsız değişkenlerini içerir operasyonlarının her biri, varsayılan ayara göre daha zayıf garantiler sağlar. dolaylı, memory_order_seq_cst. memory_order_acq_rel. hem memory_order_acquire hem de Atomik okuma-değiştirme yazma için memory_order_release garantileri anlamına gelir. memory_order_consume henüz yeterli değil iyi tanımlanmış veya uygulanmış olduğu için şimdilik yoksayılmalıdır.

Java.util.concurrent.atomic içindeki lazySet yöntemleri C++ memory_order_release mağazalarına benzer. Java'nın normal değişkenler bazen memory_order_relaxed erişimleri var. daha da zayıf hale gelebilir. C++'ın aksine, sıralanmamış işlemler için gerçek bir mekanizma yoktur volatile olarak tanımlanan değişkenlere erişir.

Bunu yapmak için acil performans nedenleri olmadıkça genellikle bunlardan kaçınmalısınız. nasıl kullanacağınızı göstereceğim. ARM gibi zayıf sıralanmış makine mimarilerinde, bunların kullanılması genellikle her atomik işlem için birkaç düzine makine döngüsünden tasarruf eder. x86'da performans kazancı mağazalarla sınırlıdır ve büyük olasılıkla daha düşük olacaktır. fark edilebilir. Daha büyük çekirdek sayılara kıyasla faydanın azalabileceği gibi, ve böylece bellek sistemi daha sınırlayıcı bir faktör haline gelir.

Zayıf sıralanmış atomların tam anlamları karmaşıktır. Genel olarak, daha iyi anlayacağız. Bu da buraya giremezsiniz. Örnek:

  • Derleyici veya donanım memory_order_relaxed verilerini taşıyabilir kilitle sınırlanmış kritik bir bölüme erişir (ancak bu bölümün dışına çıkmaz) ve serbest bırakmalısınız. Bu durumda iki memory_order_relaxed mağazanın sırası bozulabilir, olsalar bile önemli bir bölümle ayrılmış olsalar bile.
  • Paylaşılan bir sayaç olarak kötüye kullanıldığında sıradan bir Java değişkeni görünebilir yalnızca tek bir artışla olmasına rağmen azaltılacak başka bir ileti dizisine diğer ileti dizisinde. Ancak bu, C++ atomik yapısı memory_order_relaxed

Bununla birlikte, Burada, kullanımın çoğunu kapsayan az sayıda deyim sıradan atomlar için örnekler verir. Bunların çoğu yalnızca C++ için geçerlidir.

Yarış dışı erişimler

Değişkenler bazen atomik olduğundan oldukça yaygındır. okuma işlemi yapılır, ancak tüm erişimlerde bu sorun yaşanmamıştır. Örneğin, bir değişken önemli bir bölümün dışında okunduğu için atomik olması gerekebilir, ancak bir kilitle korunur. Böyle bir durumda, bu tür bir okuma aynı kilitle korunur yazma işlemi eş zamanlı olmadığından yarışamaz. Böyle bir durumda, yarış dışı erişim (bu örnekte yük), memory_order_relaxed işlemini, C++ kodunun doğruluğunu değiştirmeden kullanabilirsiniz. Kilit uygulaması, gereken bellek sıralamasını zaten zorunlu kılar diğer ileti dizileri üzerinden erişim açısından ve memory_order_relaxed esasen hiçbir ek sıralama kısıtlamasının gerekmediğini belirtir. atom erişimi için zorunlu kılınmıştır.

Java'da bunun gerçek bir benzerliği yok.

Doğruluk açısından sonuca güvenilmez

Yarış yükünü yalnızca ipucu oluşturmak için kullandığımızda yüklemeye gerek yoktur. Değer ile ilgili çıkarımlarda bulunmak için sonucu güvenilir bir şekilde kullanamayız. kullanabilirsiniz. Dolayısıyla herhangi bir sorun Bu durumda bellek sıralaması garanti edilmez ve memory_order_relaxed bağımsız değişkeniyle birlikte sağlanır.

Yaygın bir Bunun örneği C++ compare_exchange kullanımıdır ifadesini x yerine f(x) yazın. f(x) işlevini hesaplamak için x ilk yükü güvenilir olması gerekmez. Bir yanlışlık yaparsak compare_exchange başarısız olacak ve işlemi tekrar deneyeceğiz. İlk x yüklemesinde kullanılabilir. memory_order_relaxed bağımsız değişkeni yalnızca bellek sıralama bir compare_exchange arayın.

Anomik olarak değiştirilmiş ancak okunmamış veriler

Zaman zaman veriler birden çok iş parçacığı tarafından paralel olarak değiştirilebilir ancak emin olun. İyi Buna örnek olarak atomik artışlı bir sayaç gösterilebilir (ör. C++ uygulamasında fetch_add() kullanarak veya atomic_fetch_add_explicit() C) yazabilirsiniz, ancak bu çağrıların sonucunda her zaman yoksayılır. Elde edilen değer yalnızca sonunda okunur, Google Chat'i kullanmaya devam edebilirsiniz.

Bu durumda, bu verilere erişip erişemeyeceğinin yeniden sıralandığı için C++ kodu bir memory_order_relaxed kullanabilir bağımsız değişkeninin önüne geçer.

Basit etkinlik sayaçları bunun yaygın bir örneğidir. çok yaygın olduğu için bu durumla ilgili bazı gözlemler yapmakta fayda vardır:

  • memory_order_relaxed kullanımı performansı artırır, ancak en önemli performans sorununu ele almayabilir: Her güncelleme sayacı içeren önbellek satırına özel erişim gerektirir. Bu yeni bir iş parçacığı sayaca her eriştiğinde önbellekte eksikliklere neden olur. Güncellemeler sık aralıklarla ve sırayla ileti dizileri arasında değişiyorsa çok daha hızlıdır. sayacın her zaman güncellenmesini önlemek için örneğin, iş parçacığı yerel sayaçları kullanma ve bunları sonda toplama.
  • Bu teknik önceki bölümle birleştirilebilir: yaklaşık ve güvenilir olmayan değerleri eş zamanlı olarak okur. memory_order_relaxed kullanan tüm işlemlerle. Ancak ortaya çıkan değerlerin tamamen güvenilir olmayan olarak değerlendirilmesi önemlidir. Sayıda bir kez artış görülmüş gibi görünmesi, hedefe ulaşmak için başka bir ileti dizisinin sayılabileceği anlamına gelir. hangi düzeyde artırılır? Artış, önceki kodla yeniden sıralanmıştır. (Bahsettiğimiz benzer durum için C++ daha önce, böyle bir sayacın ikinci bir yüklemesinin otomatik olarak aynı iş parçacığında önceki bir yüklemeden daha az bir değer döndürür. Şu değilse: tabii ki sayaç da taştı.)
  • Yaklaşık hesaplama yapmaya çalışan kodların bulunması sık karşılaşılan bir durumdur bağımsız atomik (veya değil) okuma ve yazma işlemleri gerçekleştirerek sayaç değerleri artışı bütün bir atomik olarak yapmamaktır. Bunun normal olduğu söylenebilir. bu "yeterince yakın" performans sayaçları ve benzeri için kullanılır. Genelde öyle değildir. Güncellemeler yeterince sık olduğunda (sorun, önem verdiğinizi gösterir), sayımların büyük bir kısmı emin olun. Dört çekirdekli bir cihazda, sayıların yarısından fazlası genellikle kaybolabilir. (Kolay alıştırma: Sayacın gösterildiği iki iş parçacıklı bir senaryo oluşturun. kez güncellenir, ancak son sayaç değeri bir tanedir.)

Basit yaygın iletişim

Bir memory_order_release deposu (veya okuma-değiştirme-yazma işlemi) art arda memory_order_acquire yüklemesi durumunda (veya okuma-değiştirme-yazma işlemi) yazılı değeri okursa 2008'den önceki mağazaları (normal veya atomik) memory_order_release mağazası. Bunun aksine, memory_order_release öncesinde herhangi bir değişiklik yapılmayacak memory_order_acquire yüklemesini takip eden mağaza. memory_order_relaxed işlevinin aksine bu, bu tür atomik işlemlere izin verir. bir ileti dizisinin ilerlemesini diğerine bildirmek için kullanılır.

Örneğin, aynı durumdaki iki kez kontrol edilmiş kilitleme örneğini C++'ta yukarıdan:

class MyClass {
  private:
    atomic<Helper*> helper {nullptr};
    mutex mtx;
  public:
    Helper* getHelper() {
      Helper* myHelper = helper.load(memory_order_acquire);
      if (myHelper == nullptr) {
        lock_guard<mutex> lg(mtx);
        myHelper = helper.load(memory_order_relaxed);
        if (myHelper == nullptr) {
          myHelper = new Helper();
          helper.store(myHelper, memory_order_release);
        }
      }
      return myHelper;
    }
};

Edinme yükleme ve sürüm deposu, boş olmayan bir değer gördüğümüzde helper içeriyorsa, alanlarının doğru şekilde başlatıldığını da görürüz. Ayrıca, yarış dışı araba yüklemelerinin trafikle ilgili memory_order_relaxed kullanabilir.

Bir Java programcısı helper öğesini bir sunucu olarak java.util.concurrent.atomic.AtomicReference<Helper> ve sürüm mağazası olarak lazySet() kullanın. Yük işlemleri düz get() çağrılarını kullanmaya devam eder.

Her iki durumda da, performans düzenlememiz yalnızca başlatma aşamasında performans açısından kritik öneme sahip olma olasılığı düşük olan bir durumdur. Daha okunabilir bir uzlaşma şöyle olabilir:

    Helper* getHelper() {
      Helper* myHelper = helper.load(memory_order_acquire);
      if (myHelper != nullptr) {
        return myHelper;
      }
      lock_guard&ltmutex> lg(mtx);
      if (helper == nullptr) {
        helper = new Helper();
      }
      return helper;
    }

Bu da aynı hızlı yolu sağlar ancak varsayılana döner. açısından kritik olmayan, yavaş yavaş çalışmasındaki yol'a dokunun.

Burada bile helper.load(memory_order_acquire) mevcut Android destekli cihazlarda aynı kodu oluşturma olasılığı yüksek bir düz (sıralı olarak tutarlı) referans olarak helper. Bu, en büyük faydaya sahip optimizasyon. myHelper, bir öğeyi ortadan kaldırmak için gelecekteki bir derleyici bunu otomatik olarak yapabilir.

Satın alma/yayın siparişi verme, mağazaların görünür olmasını engellemiyor. gecikir ve mağazaların diğer ileti dizilerine görünür olmasını sağlamaz. emin olmanız gerekir. Sonuç olarak, karmaşık ve karmaşık ancak Dekker'ın karşılıklı dışlama örneğiyle gösterilen oldukça yaygın bir kodlama kalıbı algoritma: Tüm ileti dizileri, önce bu işlemi gerçekleştirmek istediklerini belirten bir işaret bir şey; t ileti dizisi, güvenli bir yol izlerse, orada olduğunu bilerek herhangi bir müdahale söz konusu olmaz. Başka hiçbir ileti dizisinde t'nin işareti hâlâ ayarlı olduğundan devam edebilirsiniz. Bu işlem başarısız olur Satın alma/yayınlama sıralaması kullanılarak bayrağa erişilirse bir ileti dizisinin işaretini, bir ileti dizisinin işaretini karar verdiysek. Varsayılan memory_order_seq_cst önlediğini unutmayın.

Sabit alanlar

Bir nesne alanı ilk kullanımda başlatılır ve daha sonra hiç değiştirilmediyse ilk kullanıma hazırlama ve daha sonra bunu yavaş yavaş sıralanan erişimdir. C++'ta atomic olarak tanımlanabilir ve memory_order_relaxed veya Java kullanılarak erişildiğinde volatile olmadan tanımlanıp erişilebilir. önlemler alıyor. Bunun için aşağıdaki muhafazaların tümü gereklidir:

  • Alanın değerinden kolayca anlaşılabilmelidir. başlatılıp başlatılmadığı. Alana erişmek için, hızlı yol test ve dönüş değeri, alanı yalnızca bir kez okumalıdır. Java'da ikinci şart önemlidir. Alan, başlatılmış olarak test etse bile ikinci bir yükleme, önceki başlatılmamış değeri okuyabilir. C++ ürününde "bir kez okunur" iyi bir uygulamadır.
  • Hem başlatma hem de sonraki yüklemeler atomik olmalıdır. emin olun. Java için, alan long veya double olmamalıdır. C++ için atom ataması gerekir; işe yaramaz çünkü atomic inşası atomik değildir.
  • Birden fazla iş parçacığı olduğundan, tekrarlanan başlatma işlemleri güvenli olmalıdır başlatılmamış değeri eşzamanlı olarak okuyabilir. C++'ta bu genellikle "basit bir şekilde kopyalanabilir" ve bu kapsamdaki tüm çalışanların atom türleri; sahip olunan iç içe işaretçi bulunan türler anlaşmalı yer kopya oluşturucu değildir ve kolayca kopyalanamaz. Java için bazı referans türleri kabul edilebilir:
  • Java referansları yalnızca son halini içeren sabit türlerle sınırlıdır alanları. Sabit türün oluşturucusu yayınlamamalıdır bir referans oluşturur. Bu durumda, Java son alan kuralları bir okuyucu referansı görürse aynı zamanda başlatılmış son alanlar. C++, bu kurallara benzemez ve Bu nedenle bu nesnelere işaret eden öğeler de kabul "özgün şekilde kopyalanabilir" ) gerekir.

Kapanış notları

Bu belge yalnızca yüzeyi çizmekten ibaret olmasa da yüzeysel bir boğazdan daha fazlasını yönetmenizi sağlar. Bu, çok kapsamlı ve derin bir konu. Biraz daha fazla keşfedilecek alanlar:

  • Gerçek Java ve C++ bellek modelleri bir happens-before (her iki işlemin garanti edildiği durumlarda) ilişkisi belirli bir sırada gerçekleşmesini sağlar. Bir veri ırkı tanımımızda "eş zamanlı" iki hafıza erişiminden bahsettik. Resmî olarak bu, iki durumun birbirinden önce olmaması anlamına gelir. gerçekten-önce olayın gerçek tanımlarını öğrenmek ve Java veya C++ Bellek Modeli'nde synchronizes-with olur. Her ne kadar sezgisel olarak “eş zamanlı” genelde iyidir Bu tanımlar yol göstericidir, özellikle de C++'ta zayıf sıralı atom işlemleri kullanmayı düşünüyor. (Geçerli Java spesifikasyonu yalnızca lazySet() kodunu tanımlar çok gayri resmî bir dille konuşabiliriz.)
  • Kodu yeniden sıralarken derleyicilerin hangi işlemleri yapmasına izin verilmediğini keşfedin. (JSR-133 spesifikasyonunda beklenmedik sonuçlar olabilir.)
  • Java ve C++ uygulamalarında sabit sınıfları nasıl yazacağınızı öğrenin. (Dahası da “inşaattan sonra hiçbir şeyi değiştirmeyin” anlamına gelmez.)
  • Etkili Sonuçlar bölümünün Eşzamanlılık bölümündeki önerileri dahili olarak kullanın. Java, 2. Sürüm. (Örneğin, senkronize edilmiş bir blok içinde geçersiz kılınması amaçlanmıştır.)
  • Kullanabileceğiniz özellikleri görmek için java.util.concurrent ve java.util.concurrent.atomic API'lerini okuyun. Şu özelliklerden faydalanabilirsiniz: @ThreadSafe ve @GuardedBy (net.jcip.annotations adresinden).

Ekteki Daha Fazla Bilgi bölümünde dokümanları ve web sitelerini kullanıma sunuyoruz.

Ek

Senkronizasyon depolarını uygulama

(Bu, çoğu programcının uygulayacağı bir şey değildir. çok aydınlatıcıdır.)

int gibi küçük yerleşik türler ve Android, normal yükleme ve mağaza talimatları, mağazanın bir uyarının, içeriğin tamamı veya hiçbir işlemci aynı konumu yüklüyor. Burada bazı temel kavramlar, "atomicity" [atomikliği] ücretsiz olarak sunulur.

Daha önce gördüğümüz gibi bu yeterli değildir. Düzenli olarak tutarlılığı sağlamak; işlemlerin yeniden sıralanmasını önlemek ve tutarlı bir şekilde diğer süreçler tarafından görünür hale gelmesini sağlar. sipariş. İkincisi de Android destekli cihazlarda otomatik olarak görünüyor. mantıklı seçimler yapmamız ve elimizdeki adımları uygulayarak güvenlik O yüzden burada çoğunlukla göz ardı ediyoruz.

Bellek işlemlerinin sırası, yeniden sıralamanın önüne geçerek korunur. ve donanımın yeniden sıralamayı önlemesini engeller. Burada odaklanacağımız tercih edebilirsiniz.

ARMv7, x86 ve MIPS'de bellek siparişi "çit" bu adımların çitleri takip eden talimatların görünmesini yaklaşık olarak engelle çitin önündeki talimatlardan önce. (Bunlar ayrıca genellikle "bariyer" olarak adlandırılır ancak bu durum, olasılık Çok daha fazlasını yapan pthread_barrier tarzı bariyerler daha fazla bilgi edinin.) Arama teriminin ele alınması gereken epey karmaşık bir konudur. farklı türde çitlerin sağladığı garantiyi bunların genellikle diğer sipariş garantileriyle nasıl birleştiğini donanım tarafından sağlanır. Bu genel hatlarıyla anlatacağım. Bu nedenle çok kolaylaşır.

En temel sipariş garantisi türü, C++ memory_order_acquire memory_order_release atomik işlemler: Sürüm deposundan önceki bellek işlemleri sonra görünür olmalıdır. ARMv7'de bu, zorunlu kılan:

  • Mağaza talimatının öncesinde uygun bir çit talimatı verin. Bu, önceki tüm bellek erişimlerinin mağaza talimatlarıdır. (Ayrıca, aynı zamanda yeniden sıralamanın bakın.)
  • Uygun bir çit talimatıyla yükleme talimatını izleyerek erişimin sonraki erişimlerle yeniden sıralanmasını önler. (En azından erken yüklemelerde gereksiz sıralamaya neden olur.)

Bunlar birlikte C++ satın alma/yayın siparişi için yeterlidir. Java volatile için gerekli olsa da yeterli değildirler veya C++ sıralı olarak tutarlı atomic.

Başka nelere ihtiyacımız olduğunu görmek için Dekker algoritmasının parçasına bakabilirsiniz. RACI matrisini elinizin altında bulundurun. flag1 ve flag2 C++ atomic veya Java volatile değişkenleri, her ikisi de başlangıçta yanlış.

İş parçacığı 1 İş parçacığı 2
flag1 = true
if (flag2 == false)
    critical-stuff
flag2 = true
if (flag1 == false)
    critical-stuff

Ardışık tutarlılık, bir ödeve yapılan atamalardan birinin flagn önce yürütülmelidir ve diğer ileti dizisinde test edin. Dolayısıyla, hiçbir zaman bu iş parçacıklarının "kritik öğeleri" aynı anda yürütmesi gerekir.

Ancak satın alma sürümü sıralaması için gereken kullanmaya başlayabilirsiniz. burayı tıklayın. Ayrıca, belirli bir kullanıcının volatile/atomic mağaza takip ediliyor volatile/atomic yüklemesi yapılıyorsa ikisi de yeniden sıralanmaz. Bu, normalde hemen önüne bir çit eklenerek değil, tutarlı bir mağaza izlemeniz gerekir. (Bu çit genellikle sipariş verdiğinden, gerekenden çok daha güçlüdür çünkü sonraki tüm bellek erişimlerine kıyasla).)

Bunun yerine, ek çitleri sıralı olarak tutarlı yükleme sayısı. Mağazalar daha az olduğu için kongre Android'de daha yaygın olup kullanıldığını görüyoruz.

Önceki bölümden birinde gördüğümüz gibi, indirme işlemine bir mağaza/yükleme engeli isteyebilirsiniz. Değişken erişim için sanal makinede yürütülen kod aşağıdaki gibi görünür:

değişken yük değişken mağaza
reg = A
fence for "acquire" (1)
fence for "release" (2)
A = reg
fence for later atomic load (3)

Gerçek makine mimarileri genellikle birçok farklı tür farklı erişim türlerini sıralayan ve farklı maliyet oluşturabilirsiniz. Bu iki unsur arasındaki seçimler çok kolay değildir ve kullanıcıları mağazaların da diğer çekirdeklere görünür olmasını sağlama ihtiyacıyla ve hafızalarda verilen hafıza sırasının doğru şekilde oluşturulmasını sağlar. Daha ayrıntılı bilgi için lütfen University of Cambridge sayfasına bakın atomların gerçek işlemcilerle eşlemelerini toplamak.

Özellikle x86 olmak üzere bazı mimarilerde "edinme" ve "release" bariyerler gerekmez, çünkü donanım her zaman ve yeterli sipariş verilmesini sağlar. Dolayısıyla, x86'da yalnızca son kare (3) üretildiğini göreceksiniz. Benzer şekilde, x86'da atomik okuma-modify-yazma sağlam bir çit eklemesi gerekir. Dolayısıyla bu hiçbir zaman gerekebilir. ARMv7'de yukarıda bahsettiğimiz tüm parmaklıklar gereklidir.

ARMv8, LDAR ve STLR talimatlarını sunar. Java değişken veya C++ sıralı olarak tutarlı gereksinimleri uygulamak ve depolar. Bu önlemler, yeniden sıralamada daha fazla zaman kullanılabilir. ARM'daki 64 bit Android kodları şunları kullanır: biz bu nedenle ARMv7 çit yerleşimine odaklanmayı bir çizgi gibidir.

Daha fazla bilgi

Daha fazla derinlik veya kapsam sağlayan web sayfaları ve dokümanları. Genel olarak ne kadar faydalı üst sıralarda gösterilir.

Paylaşılan Bellek Tutarlılığı Modelleri: Eğitim
1995'te Adve ve Gharachorloo, bellek tutarlılığı modellerini daha ayrıntılı olarak incelemek istiyorsanız iyi bir başlangıç noktasıdır.
http://www.hpl.hp.com/techreports/Compaq-DEC/WRL-95-7.pdf
Bellek Bariyerleri
Sorunları özetleyen güzel kısa bir makale.
https://en.wikipedia.org/wiki/Memory_barrier
İleti Dizileriyle İlgili Temel Bilgiler
Hans Boehm'dan C++ ve Java'da çok iş parçacıklı programlamaya giriş. Veri yarışlarını ve temel senkronizasyon yöntemlerini açıklama.
http://www.hboehm.info/c++mm/threadsintro.html
Uygulamada Java Eşzamanlılığı
2006'da yayınlanan bu kitap çok çeşitli konuları ayrıntılı olarak ele alıyor. Java'da çok iş parçacıklı kod yazan herkes için önemle tavsiye edilir.
http://www.javaconcurrencyinpractice.com
JSR-133 (Java Bellek Modeli) ile ilgili SSS
Senkronizasyon, değişken değişkenler ve son alanların oluşturulması hakkında açıklamaların yer aldığı, Java bellek modeline giriş bölümü. (Özellikle diğer dillerden bahsedildiğinde biraz eski.)
http://www.cs.umd.edu/~pugh/java/memoryModel/jsr-133-faq.html
Java Bellek Modelinde Program Dönüşümlerinin Geçerliliği
Sorunun mevcut hali ile ilgili daha teknik bir açıklama Java bellek modeli. Bu sorunlar veri ırkı içermeyen cihazlar için geçerli değildir programlarında yer alır.
http://citationseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.112.1790&rep=rep1&type=pdf
java.util.concurrent paketine genel bakış
java.util.concurrent paketiyle ilgili dokümanlar. Sayfanın alt kısmına yakın bir yerde, çeşitli sınıfların sağladığı garantilerin açıklandığı "Bellek Tutarlılığı Özellikleri" başlıklı bir bölüm bulunur.
java.util.concurrent Paket Özeti
Java Teorisi ve Uygulaması: Java'da Güvenli İnşaat Teknikleri
Bu makalede, nesne oluşturma sırasında çıkış yapan referansların tehlikeleri ayrıntılı olarak incelenmiş ve iş parçacığı açısından güvenli oluşturucular için yönergeler sağlanmaktadır.
http://www.ibm.com/developerworks/java/library/j-jtp0618.html
Java Teorisi ve Uygulaması: Değişkenliği Yönetme
Java'daki değişken alanlarla neleri yapıp neleri başaramayacağınızı açıklayan güzel bir makale.
http://www.ibm.com/developerworks/java/library/j-jtp06197.html
"Çift Kontrollü Kilitleme Bozuk" Beyanı
Bill Pugh’un, volatile veya atomic olmadan tekrar kontrol edilmiş kilitlemenin bozulduğu çeşitli yöntemler hakkında ayrıntılı açıklaması. C/C++ ve Java dahildir.
http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html
[ARM] Bariyer Litmus Testleri ve Tarif Kitabı
ARM SMP sorunlarına değinilen ve ARM kodundan oluşan kısa snippet'lerle açıklanır. Bu sayfadaki örneklerin yeterince özel olmadığını düşünüyorsanız veya DMB talimatının resmi açıklamasını okumak istiyorsanız bunu okuyun. Ayrıca, yürütülebilir koddaki bellek bariyerleri için kullanılan talimatları da açıklar (anında kod oluşturuyorsanız büyük olasılıkla kullanışlıdır). Bunun, ARMv8'den eski olduğunu unutmayın. desteklemektedir ve biraz daha güçlü bir olabilir. (Ayrıntılar için "ARM® Mimari Referans Kılavuzu ARMv8, ARMv8-A mimari profili için" bölümüne bakın.)
http://infocenter.arm.com/help/topic/com.arm.doc.genc007826/Barrier_Litmus_Tests_and_Cookbook_A08.pdf
Linux Kernel Bellek Bariyerleri
Linux çekirdek bellek engelleri ile ilgili dokümanlar. Yararlı bazı örnekler ve ASCII çizimleri içerir.
http://www.kernel.org/doc/Documentation/memory-barriers.txt
ISO/IEC JTC1 SC22 WG21 (C++ standartları) 14882 (C++ programlama dili), bölüm 1.10 ve madde 29 ("Atomik işlemler kitaplığı")
C++ atomik çalışma özellikleri için taslak standart. Bu sürüm bu alandaki küçük değişiklikleri içeren C++14 standardına yakın C++11'den geliyor.
http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2015/n4527.pdf
(giriş: http://www.hpl.hp.com/techreports/2008/HPL-2008-56.pdf)
ISO/IEC JTC1 SC22 WG14 (C standartları) 9899 (C programlama dili) bölüm 7.16 ("Atomics <stdatomic.h>")
ISO/IEC 9899-201x C atomik çalışma özellikleri için taslak standart. Ayrıntılar için daha sonra ortaya çıkan kusur raporlarını da inceleyin.
http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1570.pdf
İşleyenlere C/C++11 eşlemeleri (Cambridge Üniversitesi)
Jaroslav Sevcik ve Peter Sewell'ın çeviri koleksiyonu diyagramını birleştiriyor.
http://www.cl.cam.ac.uk/~pes20/cpp/cpp0xmappings.html
Dekker algoritması
"Eşzamanlı programlamada karşılıklı hariç tutma sorununun bilinen ilk doğru çözümü". Vikipedi'deki makalede, algoritmanın modern optimizasyon yapan derleyiciler ve SMP donanımıyla çalışmak için nasıl güncellenmesi gerektiği üzerine tartışmalarla birlikte tam algoritma bulunmaktadır.
https://tr.wikipedia.org/wiki/Dekker's_algorithm
ARM ile alfa arasındaki yorumlar ve bağımlılıkları ele alın
Catalin Marinas'tan gelen kol çekirdekli posta listesiyle ilgili bir e-posta. Adres ve kontrol bağımlılıklarının güzel bir özetini içerir.
http://linux.derkeiler.com/Mailing-Lists/Kernel/2009-05/msg11811.html
Tüm Programcıların Bellek Hakkında Bilmesi Gerekenler
Ulrich Drepper'ın farklı bellek türleri, özellikle de CPU önbellekleri hakkında çok uzun ve ayrıntılı bir makalesi.
http://www.akkadia.org/drepper/cpumemory.pdf
ARM zayıf tutarlı bellek modeli hakkında akıl yürütme
Bu makaleyi Chong ve ARM, Ltd.'den Ishtiaq. ARM SMP bellek modelini ayrıntılı ama erişilebilir bir şekilde tanımlamaya çalışıyor. Burada kullanılan "gözlemlenebilirlik" tanımı bu makaleden gelmektedir. Bu da ARMv8'den öncedir.
http://portal.acm.org/ft_gateway.cfm?id=1353528&type=pdf&coll=&dl=&CFID=96099715&CFTOKEN=57505711
Derleyici Yazarlar için JSR-133 Kılavuzu
Doug Lea bunu JSR-133 (Java Bellek Modeli) dokümanlarının tamamlayıcısı olarak yazdı. İlk dizi uygulama yönergelerini içerir pek çok derleyici yazarı tarafından kullanılan Java bellek modeli için yaygın olarak alıntı yapılan ve faydalı bilgiler sağlayan yeni kaynaklar olduğunu tespit ettik. Ne yazık ki burada tartışılan dört çit türü iyi değil. Eşleşmesi; Android destekli mimariler ve yukarıdaki C++11 eşlemeleri için artık Java için bile daha iyi bir tarif kaynağı oldu.
http://g.oswego.edu/dl/jmm/recipebook.html
x86-TSO: x86 Çok İşlemcileri İçin Titiz ve Kullanılabilir Bir Programcı Modeli
x86 bellek modelinin tam açıklaması. Konuyla ilgili titiz açıklamalar ARM bellek modeli maalesef çok daha karmaşıktır.
http://www.cl.cam.ac.uk/~pes20/weakmemory/cacm.pdf