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 |
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. 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
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. 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
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ı gerektirir.) 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 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 = ...
|
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
. 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 = truereg0 = flag; while (!reg0) {}
... = Aflag
doğru olmasına rağmen döngünün sonsuza kadar devam ettiğini görebilirsiniz. - 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 konulara aşağıda kısaca değineceğiz. - 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ı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:
reg = mValue
reg = reg + 1
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:
- 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.)
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 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ı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<mutex> 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
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ığı, 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
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ş. İ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 |
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. 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 "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 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
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ı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