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 |
reg0 = B |
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
veyapthread_mutex_t
) veya Java'dakisynchronized
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
veyaunique_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 sahipatomic
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
veyaatomic
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 = ...
|
while (!flag) {}
|
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:- 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 = truereg0 = flag; ancak (!reg0) {}
... = Aflag
doğrudur. - 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 özelliklelazySet()
’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. - 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 yerineatomic
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:
reg = mValue
reg = reg + 1
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:
- Basit olanı yapın ve dış kontrolü silin. Bu sayede hiçbir zaman
senkronize edilmiş bir blokun dışındaki
helper
değerini inceleyin. 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 ikimemory_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<mutex> 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
veyadouble
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
vejava.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 |
flag2 = true |
Ardışık tutarlılık, bir ödeve yapılan atamalardan birinin
flag
n ö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 "release" (2) |
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
veyaatomic
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