Android için SMP yardımcı programı

Android 3.0 ve sonraki platform sürümleri, çok işlemci mimarilerini desteklemek için optimize edilmiştir. 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. Çok iş parçacıklı bir programdaki yarış koşulları, tek işlemcili bir sistemde görünür sorunlara neden olmayabilir ancak iki veya daha fazla iş parçacığınız farklı çekirdeklerde aynı anda çalışırken düzenli olarak hata verebilir. 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, neden bu şekilde davrandığı açıklanacak ve kodunuzun doğru şekilde çalışmasını sağlamak için yapmanız gerekenler belirtilecektir.

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ık modelleri veya genellikle "bellek modelleri", programlama dilinin veya donanım mimarisinin bellek erişimleri hakkında verdiği garantileri açıklar. Ö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ı açıklamak için genellikle litmus testleri olarak adlandırılan küçük kod snippet'lerini incelemek faydalı olacaktır.

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

Mesaj dizisi 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. iş parçacığı, A konumunda 3 değerini, ardından B konumunda 5 değerini depolar. İş 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ı)
reg0=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 olmak üzere ç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 değişiklik 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. Arabelleğe alınan depolama alanlarının sırası korunmaz ve depolama alanları diğer tüm çekirdeklere aynı anda erişemeyebilir. 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 talimatların sırasını değiştirir. 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'i seçin. 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 dikkatle bu durumun ç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ışından arındırılmış" programlama stili olarak bilinen stili teşvik eder. "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ı, bellek yeniden sıralama hakkındaki çirkin gerçeği ortaya çıkarır.

"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. "Sıradan veri" derken, özellikle mesaj dizisi iletişimi için tasarlanmış bir senkronizasyon nesnesi olmayan bir öğeyi kastediyoruz. 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 ve B başlangıçta yanlış olan normal boole değişkenleriyse aşağıdaki programda veri yarışı yoktur:

İş parçacığı 1 Mesaj dizisi 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. Bir int'ye atamayı aynı anda 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, bir iş parçacığında bir set<T> güncellenirken başka bir iş parçacığında farklı bir set<T> okunması veri yarışına neden olmaz. Çünkü kitaplık bu durumda (düşük düzeyli) veri yarışı oluşturmayacağını taahhüt eder.

Bir veri yapısındaki farklı alanlara normalde eşzamanlı erişimler veri yarışı başlatamaz. Ancak bu kuralın önemli bir istisnası vardır: C veya C++'taki bit alanı dizileri tek bir "bellek konumu" olarak değerlendirilir. Böyle bir dizideki herhangi bir bit alanına erişme yalnızca, geçerli olan üçüncü tarafların ihtiyaçlarını belirleme amacıyla 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 Mutex'ler
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. Ortak bir veri yapısına erişmeden önce belirli bir kilidi tutarlı bir şekilde edinip daha sonra bırakmak, veri yapısına erişirken veri yarışlarını önler. 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. Bu, veri yarışlarını önlemek için en yaygın araçtır. 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ı gerektirir.) 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 yöntem artık önerilmez. C++'da, birden fazla iş parçacığı tarafından eşzamanlı olarak erişilebilecek değişkenler için atomic<T> kullanın. 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 Mesaj dizisi 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. Ya da derleyici, işaretin 2. iş parçacığının döngüsü sırasında değişemeyeceğine karar verip programı şu şekilde dönüştürebilir:
    İş parçacığı 1 İş parçacığı 2
    A = ...
      flag = true
    reg0 = flag; while (!reg0) {}
    ... = A
    Hata ayıklama yaparken, flag doğru olmasına rağmen döngünün sonsuza kadar devam ettiğini görebilirsiniz.
  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 konulara aşağıda kısaca değineceğiz.
  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ık sorunlarında hata ayıklama ç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. Bunu yapmadan önce temel bir dil özelliğinin kullanımını incelememiz gerekir.

C/C++ ve "volatile"

C ve C++ volatile bildirimleri çok özel amaçlı bir araçtır. Derleyicinin geçici erişimleri yeniden sıralamasını veya kaldırmasını engeller. 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.

Eski C ve C++ kodlarının çoğunda, iş parçacığı iletişimi için volatile hâlâ kötüye kullanılmaktadır. Bu yöntem, açık çitlerle veya bellek sıralamasının önemli olmadığı durumlarda kullanıldığında genellikle makine kaydedicisine sığabilecek veriler için doğru çalışır. 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. volatile alanlarına eşzamanlı erişime izin verilir ve bu erişim veri yarışlarına neden olmaz. 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. iş parçacığı bir volatile alanına yazarsa ve 2. iş parçacığı daha sonra aynı alandan okuyup yeni yazılan değeri görürse 2. iş parçacığının, 1. iş parçacığı tarafından daha önce yapılan tüm yazma işlemlerini de görmesi garanti edilir. Bellek etkisi açısından, geçici bir değişkene yazmak, monitör serbest bırakma işlemine benzer. Geçici bir değişkenden okumak ise monitör edinme işlemine 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

Monotonik sayıcı için basit ve yanlış bir uygulama aşağıda verilmiştir: (Java teorisi ve uygulaması: Dalgalanmayı yönetme).

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

get() ve incr()'un birden fazla iş parçacığında çağrıldığını varsayalım. get() çağrıldığında her iş parçacığının mevcut sayıyı gördüğünden emin olmak istiyoruz. En belirgin sorun, mValue++'ün aslında üç işlem olmasıdır:

  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ış işlemini atomik hale getirmek için incr()'yi "synchronized" olarak belirtmemiz gerekir.

Ancak özellikle SMP'de hâlâ sorunlu. get(), incr() ile eşzamanlı olarak mValue'a erişebildiğinden veri yarışı devam eder. Java kurallarına göre, get() çağrısı diğer koda göre yeniden düzenlenmiş gibi görünebilir. Örneğin, art arda iki sayıcı okursak get() çağrıları donanım veya derleyici tarafından yeniden sıraladığımız için sonuçlar tutarsız görünebilir. 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. Bu durum performansı olumsuz etkileyebilir. get()'ü senkronize olarak beyan etmek yerine mValue'ü "volatile" olarak beyan edebiliriz. (mValue++ tek bir atomik işlem olmadığından incr()'nin yine de synchronize kullanması gerektiğini unutmayın.) 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 olması gerekir.

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 için uçucu bir yükleme ve ardından sGoodies.x için uçucu olmayan bir yükleme gerçekleştirir. 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 her çağrıda "synchronized" bloğunun ek maliyetini ödemek istemeyiz. Bu nedenle, bu kısmı yalnızca helper şu anda null ise yaparız.

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 iş parçacığı, helper değerinin null olmadığını ancak alanlarının henüz ayarlanmadığını ve kullanıma hazır olmadığını görebilir. 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.)

volatile davranışını gösteren başka bir görsel:

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()'e bakıldığında, 2. Konu henüz vol1 güncellemesini gözlemlemediyse data1 veya data2'ın henüz ayarlanıp ayarlanmadığını bilemez. 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, tüm Android platform sürümlerinde doğru (aksi belirtilmedikçe sıralı olarak tutarlı) ve verimli davranış sağlayan uygun bellek çitleri bulunur. Bunları kullandığınızdan emin olun sağlayabilir. Örneğin, koşul değişkeni beklemelerinin sinyal gönderilmeden yanlışlıkla döndürülebileceğini ve bu nedenle bir döngüde görünmesi gerektiğini unutmayın.

Veri yapısı devre dışı olmadığı sürece, atom fonksiyonlarını doğrudan kullanmaktan son derece basit olmalı, mesela sayaç gibi. Bir pthread mutex'in kilitlenmesi ve kilidinin açılması için her biri tek bir atomik işlem gerekir ve genellikle rekabet yoksa tek bir önbelleğe alma hatasından daha az maliyetlidir. Bu nedenle, mutex çağrılarını atomik işlemlerle değiştirerek çok fazla tasarruf edemezsiniz. Ö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. Bu türleri kullanan mevcut kodun büyük bir kısmında daha sonra hata olduğu tespit edilir. Mümkünse bunlardan kaçının. Kullanım alanınız bir sonraki bölümdekilerden birine tam olarak uymuyorsa uzman olduğunuzdan veya bir uzmana danıştığınızdan emin olun.

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. Ancak bu her zaman iyi bir tavsiyedir ve Java güvenlik modelinin önemli olduğu diğer bağlamlarda Java kodunuz çalıştırılıyorsa ve güvenilmeyen kod bu "sızdırılmış" nesne referansına erişerek veri yarışına neden oluyorsa kritik hale gelir. 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. Atomik işlemler için açık memory_order_relaxed, memory_order_acquire (yalnızca yükler) ve memory_order_release (yalnızca depolar) bağımsız değişkenlerinin her biri, varsayılan (genellikle açık) memory_order_seq_cst bağımsız değişkeninden kesinlikle daha zayıf garantiler sağlar. memory_order_acq_rel, atomik okuma-değiştirme-yazma işlemleri için hem memory_order_acquire hem de memory_order_release garantileri sunar. 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ılan sıradan bir Java değişkeni, yalnızca tek bir başka iş parçacığı tarafından artırılmış olsa bile başka bir iş parçacığı için azalıyormuş gibi görünebilir. Ancak bu, C++ atomik yapısı memory_order_relaxed

Bu uyarıyı göz önünde bulundurarak, zayıf sıralı atomlar için kullanım alanlarının çoğunu kapsayan birkaç deyim paylaşıyoruz. 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 durumda yükleme), C++ kodunun doğruluğunu değiştirmeden memory_order_relaxed ile eklenebilir. 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ışmalı yükleme yalnızca ipucu oluşturmak için kullanıldığında, genellikle yükleme için herhangi bir bellek sıralaması zorunlu tutmamak da sorun olmaz. 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 tüm güncellemeler tamamlandıktan sonra son aşamada okunur.

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'ten farklı olarak bu, bir iş parçacığının ilerleme durumunu başka bir iş parçasına iletmek için bu tür atomik işlemlerin kullanılmasına olanak tanı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;
    }
};

acquire load ve release store, null olmayan bir helper görürsek alanlarının da doğru şekilde başlatılmasını sağlar. 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 ayarlamamız, performans açısından kritik olma olasılığı düşük olan başlatma yoluna odaklandı. 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 yöntem aynı hızlı yolu sağlar ancak performans açısından kritik olmayan yavaş yolda varsayılan, sıralı olarak tutarlı işlemlere başvurur.

Bu durumda bile helper.load(memory_order_acquire), helper'a basit (sıralı olarak tutarlı) bir referans olarak mevcut Android destekli mimarilerde aynı kodu oluşturma olasılığı yüksektir. 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. t işareti hâlâ ayarlandığından başka bir iş parçacığı devam edemez. 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 bunu engeller.

Değiştirilemeyen 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 edilse bile ikinci bir yükleme, daha önce başlatılmamış olan 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ığı, başlatılmamış değeri eşzamanlı olarak okuyabileceğinden, tekrarlanan başlatmalar güvenli olmalıdır. 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++'da bu kurallara benzer bir şey yoktur ve sahip olunan nesnelerin işaretçileri de bu nedenle kabul edilemez ("basitçe kopyalanabilir" koşullarını ihlal etmenin yanı sıra).

Kapanış notları

Bu doküman, konuyu yüzeysel olarak ele almaktan fazlasını yapmasa da konuyu derinlemesine incelemeyi başaramıyor. 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çlara yol açan yasal dönüşümlere dair bazı mükemmel örnekler vardır.)
  • 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 Java, 2. Baskı kitabının Eşzamanlılık bölümündeki önerileri öğrenin. (Örneğin, senkronize edilmiş bir blok içindeyken geçersiz kılınması amaçlanan yöntemleri çağırmaktan kaçınmalısınız.)
  • 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ş. İlki için doğru seçimleri yaptığımızda, ikincisi Android destekli donanımlarda otomatik olarak uygulanır. Bu nedenle, burada büyük ölçüde ikincisini göz ardı ediyoruz.

Bellek işlemlerinin sırası, hem derleyici tarafından yeniden sıralamanın hem de donanım tarafından yeniden sıralamanın engellenmesi sayesinde korunur. Burada ikincisine odaklanıyoruz.

ARMv7, x86 ve MIPS'te bellek sıralaması, "çit" talimatlarıyla zorunlu kılınmaktadır. Bu talimatlar, çitten sonraki talimatların çitten önceki talimatlardan önce görünür olmasını kabaca önler. (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, aşağıdakiler tarafından zorunlu kılınmaktadır:

  • Mağaza talimatının önüne uygun bir çit talimatı ekleyin. 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++ edinme/salma sıralaması için yeterlidir. Java volatile için gerekli olsa da yeterli değildirler veya C++ sıralı olarak tutarlı atomic.

Başka neye ihtiyacımız olduğunu görmek için daha önce kısaca bahsettiğimiz Dekker algoritmasının parçasını inceleyin. 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. Bu nedenle, bu iş parçacıklarının "kritik işlemleri" aynı anda yürüttüğünü hiçbir zaman görmeyiz.

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 çiti sırayla tutarlı yükler ile ilişkilendirebiliriz. 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. Geçici 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 seçenek arasında seçim yapmak hassas bir konudur ve mağazaların diğer çekirdeklere tutarlı bir sırada gösterilmesini ve birden fazla çitin birleşimi tarafından uygulanan bellek sıralamasının doğru şekilde oluşturulmasını sağlama ihtiyacından etkilenir. 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.

Donanım her zaman dolaylı olarak yeterli sıralamayı zorunlu kıldığından, özellikle x86 olmak üzere bazı mimarilerde "acquire" ve "release" engelleri gereksizdir. 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 çitler 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ık 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 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 yılında yayınlanan bu kitapta çok çeşitli konular ayrıntılı bir şekilde ele alınmaktadır. 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 diller hakkındaki bilgiler biraz eskidir.)
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ı: Dalgalanmaları 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ının, ARM kodunun kısa snippet'leriyle açıklandığı bir tartışma. Bu sayfadaki örnekleri çok belirsiz bulduysanı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 fıkra 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 ve Alpha ile ilgili yorumlar ve adres bağımlılıkları
Catalin Marinas'tan gelen kol çekirdekli posta listesiyle ilgili bir e-posta. Adres ve kontrol bağımlılıkları hakkında güzel bir özet 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 makale, ARM, Ltd.'den Chong ve Ishtiaq tarafından yazılmıştır. ARM SMP bellek modelini titiz ancak erişilebilir bir şekilde açıklamaya çalışmaktadır. 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. Maalesef burada açıklanan dört çit türü, Android tarafından desteklenen mimariler için iyi bir eşleşme değildir ve yukarıdaki C++11 eşlemeleri artık Java için bile hassas tarifler için daha iyi bir kaynaktır.
http://g.oswego.edu/dl/jmm/cookbook.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