C ++ 11'de normalde asla volatile
diş açma için kullanmayın , yalnızca MMIO için
Ancak TL: DR, mo_relaxed
uyumlu önbelleklere sahip donanımda atomik gibi "çalışır" (yani her şey); derleyicilerin değişkenleri kayıtlarda tutmasını durdurmak yeterlidir. atomic
atomiklik veya iş parçacığı arası görünürlük oluşturmak için bellek engellerine ihtiyaç duymaz, yalnızca geçerli iş parçacığının, bu iş parçacığının farklı değişkenlere erişimleri arasında sıralama oluşturmak için bir işlemden önce / sonra beklemesini sağlamak için. mo_relaxed
hiçbir engele ihtiyaç duymaz, sadece yükleyin, depolayın veya RMW.
C ++ 11'den önceki kötü eski günlerde kendi atomlarınızı volatile
(ve engelleri için satır içi) yuvarlamak için , bazı şeyleri çalıştırmanın tek iyi yolu buydu . Ancak uygulamaların nasıl çalıştığına dair birçok varsayıma bağlıydı ve hiçbir standart tarafından garanti edilmedi.std::atomic
volatile
Örneğin, Linux çekirdeği hala kendi elle çevrilmiş atomlarını kullanıyor volatile
, ancak yalnızca birkaç özel C uygulamasını (GNU C, clang ve belki ICC) destekliyor. Bunun nedeni kısmen GNU C uzantıları ve satır içi asm sözdizimi ve anlambilim, ama aynı zamanda derleyicilerin nasıl çalıştığına dair bazı varsayımlara bağlı olmasıdır.
Yeni projeler için neredeyse her zaman yanlış seçimdir; Eğer kullanabilirsiniz std::atomic
(ile std::memory_order_relaxed
sizinle olabilir aynı verimli makine kodu yayması için bir derleyici almak için) volatile
. diş açma amaçlı obsoletes std::atomic
ile . mo_relaxed
volatile
(belki bazı derleyicilerde eksik optimizasyon hatalarınınatomic<double>
üstesinden gelmek dışında .)
İç uygulama std::atomic
(gcc ve clang gibi) ana akım derleyici gelmez üzerinde değil sadece kullanmak volatile
içten; derleyiciler doğrudan atomik yük, depolama ve RMW yerleşik işlevlerini ortaya çıkarır. (örneğin , "düz" nesneler üzerinde çalışan GNU C __atomic
yerleşikleri .)
Uçucu pratikte kullanılabilir (ama yapmayın)
Bununla birlikte, CPU'ların nasıl çalıştığı (tutarlı önbellekler) ve nasıl çalışması gerektiğine dair paylaşılan varsayımlar nedeniyle, gerçek CPU'larda mevcut tüm (?) C ++ uygulamalarında volatile
bir exit_now
bayrak gibi şeyler için pratikte kullanılabilir volatile
. Ama çok fazla değil ve tavsiye edilmiyor . Bu cevabın amacı, mevcut CPU'ların ve C ++ uygulamalarının gerçekte nasıl çalıştığını açıklamaktır. Bunu umursamıyorsanız, tüm bilmeniz gereken, iş parçacığı için std::atomic
mo_relaxed obsoletes olmasıdır volatile
.
(ISO C ++ standardı bu konuda oldukça belirsizdir, sadece volatile
erişimlerin optimize edilmeden değil, kesinlikle C ++ soyut makinenin kurallarına göre değerlendirilmesi gerektiğini söyler. Gerçek uygulamaların C ++ adres alanını modellemek için makinenin bellek adres alanını kullandığı göz önüne alındığında, bu, volatile
okumaların ve atamaların bellekteki nesne temsiline erişmek için talimatları yüklemek / depolamak için derlenmesi gerektiği anlamına gelir .)
Başka bir yanıtın da işaret ettiği gibi, exit_now
bayrak, herhangi bir senkronizasyon gerektirmeyen basit bir iş parçacığı arası iletişim durumudur : dizi içeriklerinin hazır olduğunu veya buna benzer herhangi bir şeyi yayınlamaz. Başka bir ileti dizisindeki optimize edilmemiş bir yük tarafından hemen fark edilen bir mağaza.
// global
bool exit_now = false;
// in one thread
while (!exit_now) { do_stuff; }
// in another thread, or signal handler in this thread
exit_now = true;
Uçucu veya atomik olmadan, sanki kuralı ve hiçbir veri yarışının olmadığı varsayımı, bir derleyicinin onu sonsuz bir döngüye girmeden (veya girmeden) önce bayrağı yalnızca bir kez kontrol eden bir asm olarak optimize etmesine izin verir . Gerçek derleyiciler için gerçek hayatta olan tam olarak budur. (Ve genellikle do_stuff
döngü asla çıkmadığı için çoğunu optimize edin, bu nedenle sonucu kullanmış olabilecek daha sonraki herhangi bir koda, döngüye girersek erişilemez).
// Optimizing compilers transform the loop into asm like this
if (!exit_now) { // check once before entering loop
while(1) do_stuff; // infinite loop
}
Çok iş parçacıklı program optimize edilmiş modda kaldı, ancak normal olarak -O0'da çalışıyor, bunun x86-64'te GCC ile tam olarak nasıl gerçekleştiğinin bir örneğidir (GCC'nin asm çıktısının açıklamasıyla). Ayrıca MCU programlama - C ++ O2 optimizasyonu bozulurken elektronik devrelerdeki.SE başka bir örnek gösteriyor.
Normalde , genel değişkenler de dahil olmak üzere, CSE'nin ve yükleri döngüden kaldıran agresif optimizasyonlar istiyoruz .
C ++ 11'den önce,volatile bool exit_now
bunun amaçlandığı gibi çalışmasını sağlamanın bir yolu vardı (normal C ++ uygulamalarında). Ancak C ++ 11'de, veri yarışı UB hala geçerlidir, volatile
bu nedenle HW tutarlı önbellekleri varsayılsa bile her yerde çalışması ISO standardı tarafından garanti edilmez .
Daha geniş tipler için volatile
yırtılma eksikliği garantisi vermediğini unutmayın. bool
Normal uygulamalarda sorun olmadığı için burada bu ayrımı görmezden geldim . Ancak bu aynı zamanda volatile
, gevşetilmiş atomik ile eşdeğer olmak yerine neden hala veri yarışına UB'ye tabi olduğunun bir parçası .
"Amaçlandığı gibi" ifadesinin, iş parçacığının exit_now
diğer iş parçacığının gerçekten çıkmasını beklediği anlamına gelmediğini unutmayın . Veya exit_now=true
bu iş parçacığındaki sonraki işlemlere devam etmeden önce uçucu deponun küresel olarak görünür olmasını bile beklediğini . ( atomic<bool>
varsayılan mo_seq_cst
, en azından sonraki seq_cst yüklenmeden önce beklemesini sağlar. Birçok ISA'da, mağazadan sonra tam bir bariyer elde edersiniz).
C ++ 11, aynı şeyi derleyen UB olmayan bir yol sağlar
"Çalışmaya devam et" veya "şimdi çık" bayrağı std::atomic<bool> flag
,mo_relaxed
kullanma
flag.store(true, std::memory_order_relaxed)
while( !flag.load(std::memory_order_relaxed) ) { ... }
size alacağınız aynı asmi (pahalı engel talimatları olmadan) verecektir volatile flag
.
Yırtılmanın yanı sıra atomic
, size UB olmadan bir iş parçacığında depolama ve başka bir iş parçacığında yükleme yeteneği sağlar, böylece derleyici yükü bir döngüden kaldıramaz. (Atomik olmayan uçucu olmayan nesneler için istediğimiz agresif optimizasyonlara izin veren, veri yarışı olmayan UB varsayımıdır.) Bu özellik, saf yükler ve saf depolar için atomic<T>
olanla hemen hemen aynıdır volatile
.
atomic<T>
Ayrıca yapmak +=
ve böylece (eğer bir atom RMW istemiyorsanız, yerel bir geçici ile kod yazmak geçici, işletmek, ardından ayrı atom deposuna bir atom yükü önemli ölçüde daha pahalı.) atomik RMW operasyonları içine üzerinde.
seq_cst
Alacağınız varsayılan siparişle while(!flag)
, aynı zamanda sipariş garantileri de ekler. atomik olmayan erişimler ve diğer atomik erişimler.
(Teorik olarak, ISO C ++ standart atomics ait derleme zamanı optimizasyonu ekarte etmez. Ama pratikte derleyiciler yok Bunda sorun olmaz denetime yolu yoktur çünkü. Birkaç durumlar vardır bile volatile atomic<T>
olmayabilir Şimdi derleyiciler yok öylesine için, derleyiciler optimize yapsam atomics optimizasyonu üzerinde yeterli kontrol olun. Bkz Neden derleyiciler gereksiz std :: atom yazıyor? birleştirme yok wg21 / p0062 kullanılmasına karşı önerir not volatile atomic
optimizasyonu karşı koruma için bu koda atomik).
volatile
aslında bunun için gerçek CPU'larda çalışıyor (ama yine de kullanmıyor)
zayıf sıralı bellek modellerinde bile (x86 olmayan) . Ama aslında kullanmayın, bunun yerine atomic<T>
ile mo_relaxed
kullanın !! Bu bölümün amacı, gerçek CPU'ların nasıl çalıştığına dair yanlış anlamaları gerekçelendirmek değil volatile
. Kilitsiz kod yazıyorsanız, muhtemelen performansı önemsiyorsunuzdur. Önbellekleri ve iş parçacıkları arası iletişimin maliyetlerini anlamak, genellikle iyi performans için önemlidir.
Gerçek CPU'ların uyumlu önbellekleri / paylaşılan hafızaları vardır: bir çekirdekten bir depo küresel olarak görünür hale geldikten sonra, başka hiçbir çekirdek eski bir değer yükleyemez . (Ayrıca bkz.Mits Programcıları, seq_cst bellek sırasına sahip C ++ 'ya eşdeğer Java uçucularından bahseden CPU Önbellekleri hakkında atomic<T>
inanırlar.)
Dediğimde yükü , ben hafızayı erişen bir asm talimatı anlamına gelir. Bu, bir volatile
erişimin sağladığı şeydir ve atomik olmayan / uçucu olmayan bir C ++ değişkeninin ldeğerden r değerine dönüşümüyle aynı şey değildir . (örneğin local_tmp = flag
veya while(!flag)
).
Yenmeniz gereken tek şey, ilk kontrolden sonra hiç yeniden yüklenmeyen derleme zamanı optimizasyonlarıdır. Her yinelemedeki herhangi bir yükleme + kontrol, herhangi bir sıralama olmaksızın yeterlidir. Bu iş parçacığı ile ana iş parçacığı arasında senkronizasyon olmadan, deponun tam olarak ne zaman gerçekleştiğinden veya yükün sıralanmasından bahsetmek anlamlı değildir. döngüdeki diğer işlemler. Sadece bu ileti dizisine göründüğünde önemli olan şeydir. Exit_now bayrak setini gördüğünüzde çıkarsınız. Tipik bir x86 Xeon'daki çekirdekler arası gecikme, ayrı fiziksel çekirdekler arasında 40ns gibi bir şey olabilir .
Teoride: Uyumlu önbellekleri olmayan donanımda C ++ iş parçacıkları
Programcının kaynak kodda açık yıkamalar yapmasını gerektirmeden, sadece saf ISO C ++ ile bunun uzaktan verimli olabileceği bir yol görmüyorum.
Teoride, bunun gibi olmayan bir makinede bir C ++ uygulamasına sahip olabilirsiniz, bu da nesneleri diğer çekirdeklerdeki diğer iş parçacıkları için görünür kılmak için derleyici tarafından oluşturulan açık yıkamalar gerektirir. . (Veya okumalar için belki eski bir kopya kullanmamak için). C ++ standardı bunu imkansız kılmaz, ancak C ++ 'ın bellek modeli, tutarlı paylaşımlı bellekli makinelerde verimli olacak şekilde tasarlanmıştır. Örneğin, C ++ standardı "okuma-okuma tutarlılığı", "yazma-okuma tutarlılığı" vb. Hakkında bile konuşur. Standarttaki bir not bile donanımla bağlantıya işaret eder:
http://eel.is/c++draft/intro.races#19
[Not: Her iki işlem de gevşetilmiş yükler olsa bile, önceki dört tutarlılık gereksinimi, derleyicinin atomik işlemleri tek bir nesneye yeniden düzenlemesine etkili bir şekilde izin vermez. Bu, çoğu donanım tarafından sağlanan önbellek tutarlılığı garantisini C ++ atomik işlemler için kullanılabilir hale getirir.- son not]
Bir release
mağazanın yalnızca kendisini temizlemesi için bir mekanizma ve birkaç belirli adres aralığı yoktur: her şeyi senkronize etmesi gerekirdi, çünkü eğer alma-yükleri bu yayın deposunu görselerdi, diğer iş parçacıklarının okumak isteyebileceklerini bilemezdi (bir Yazma iş parçacığı tarafından yapılan atomik olmayan daha önceki işlemlerin artık okunmasının güvenli olduğunu garanti ederek iş parçacıkları arasında bir önce-olur ilişkisi kuran sürüm dizisi. Yayın deposundan sonra onlara daha fazla yazmadıkça ...) Veya derleyiciler sadece birkaç önbellek satırının temizlenmesi gerektiğini kanıtlamak için gerçekten akıllı olmak.
İlgili: mov + mfence NUMA'da güvenli mi? tutarlı paylaşılan bellek olmadan x86 sistemlerinin var olmaması hakkında ayrıntılara giriyor. Ayrıca ilgili: Aynı yüklemeler / depolar hakkında daha fazla bilgi için ARM üzerinde yeniden sıralama yükler ve depolar konuma .
Orada olan ben-olmayan tutarlı paylaşılan bellek ile kümeleri düşünüyorum, ama bunlar tek sistem görüntü makineleri değiliz. Her tutarlılık etki alanı ayrı bir çekirdek çalıştırır, bu nedenle tek bir C ++ programının iş parçacıkları üzerinde çalıştıramazsınız. Bunun yerine, programın ayrı örneklerini çalıştırırsınız (her birinin kendi adres alanı vardır: bir örnekteki işaretçiler diğerinde geçerli değildir).
Açık yıkamalar aracılığıyla birbirleriyle iletişim kurmalarını sağlamak için, programın hangi adres aralıklarının temizlenmesi gerektiğini belirtmesini sağlamak için tipik olarak MPI veya diğer ileti aktarım API'sini kullanırsınız.
Gerçek donanım, std::thread
önbellek tutarlılığı sınırlarının ötesine geçmez :
Paylaşılan fiziksel adres alanına sahip ancak içte paylaşılabilir önbellek etki alanlarına sahip olmayan bazı asimetrik ARM yongaları mevcuttur . Yani tutarlı değil. (örneğin, açıklama iplik bir A8 çekirdek TI Sitara AM335x gibi Cortex-M3).
Ancak bu çekirdekler üzerinde farklı çekirdekler çalışır, her iki çekirdekte de iş parçacıkları çalıştırabilen tek bir sistem görüntüsü değil. std::thread
Tutarlı önbellekler olmadan CPU çekirdeklerinde iş parçacığı çalıştıran herhangi bir C ++ uygulamasından haberdar değilim .
Özel olarak ARM için, GCC ve clang, tüm iş parçacıklarının aynı iç paylaşılabilir etki alanında çalıştığını varsayarak kod üretir. Aslında, ARMv7 ISA kılavuzu diyor ki
Bu mimari (ARMv7), aynı işletim sistemini veya hiper yöneticiyi kullanan tüm işlemcilerin aynı Dahili Paylaşılabilir paylaşılabilirlik etki alanında olması beklentisiyle yazılmıştır.
Dolayısıyla, ayrı etki alanları arasında tutarlı olmayan paylaşılan bellek, yalnızca farklı çekirdekler altındaki farklı işlemler arasındaki iletişim için paylaşılan bellek bölgelerinin açık sisteme özgü kullanımı için bir şeydir.
Ayrıca bu derleyicidedmb ish
(İç Paylaşılabilir bariyer) ve dmb sy
(Sistem) bellek engellerini kullanan kod oluşturma hakkındaki bu CoreCLR tartışmasına da bakın .
Diğer ISA'lar için hiçbir C ++ uygulamasının std::thread
tutarlı olmayan önbelleklere sahip çekirdeklerde çalışmadığını iddia ediyorum . Böyle bir uygulamanın olmadığına dair kanıtım yok, ancak bu pek olası görünmüyor. Bu şekilde çalışan belirli bir egzotik HW parçasını hedeflemediğiniz sürece, performans hakkındaki düşünceniz tüm iş parçacıkları arasında MESI benzeri önbellek tutarlılığını varsaymalıdır. (Tercihen atomic<T>
doğruluğu garanti eden şekillerde kullanın !)
Tutarlı önbellekleri basitleştirir
Ancak tutarlı önbellekleri olan çok çekirdekli bir sistemde, bir yayın deposu uygulamak, yalnızca bu iş parçacığının mağazaları için önbelleğe kaydetme siparişi vermek anlamına gelir, herhangi bir açık temizleme işlemi yapmaz. ( https://preshing.com/20120913/acquire-and-release-semantics/ ve https://preshing.com/20120710/memory-barriers-are-like-source-control-operations/ ). (Ve bir alma-yükleme, diğer çekirdekte önbelleğe erişim siparişi vermek anlamına gelir).
Bir bellek bariyer talimatı, mevcut iş parçacığının yüklemelerini engeller ve / veya depolama tamponu boşalana kadar depolar; bu her zaman kendi başına olabildiğince hızlı gerçekleşir. ( Bir bellek engeli, önbellek tutarlılığının tamamlanmasını sağlıyor mu? Bu yanlış kanıyı giderir). Bu nedenle, sipariş vermeye ihtiyacınız yoksa, diğer konu başlıklarında anında görünürlük yeterlidir mo_relaxed
. (Ve öyleysevolatile
, ama bunu yapma.)
İşlemciler için C / C ++ 11 eşlemelerine
de bakın
Eğlenceli gerçek: x86'da her asm deposu bir yayın deposu çünkü x86 bellek modeli temelde seq-cst artı bir depo tamponu (depo iletme ile).
Yarı ilişkili re: store buffer, global görünürlük ve tutarlılık: C ++ 11 çok az garanti verir. Çoğu gerçek ISA (PowerPC hariç), tüm iş parçacıklarının diğer iki iş parçacığı tarafından iki mağazanın görünüm sırasına karar verebileceğini garanti eder. (Resmi bilgisayar mimarisi bellek modeli terminolojisinde, bunlar "çok kopyalı atomiktir").
Başka bir yanlış anlama bellek çit asm talimatları diğer çekirdekler mağazalarımızı görmek için mağaza tampon temizlemek için gerekli olmasıdır hiç . Aslında depo tamponu her zaman kendisini olabildiğince hızlı boşaltmaya çalışır (L1d önbelleğine bağlanır), aksi takdirde doldurur ve yürütmeyi durdurur. Tam bir bariyerin / çitin yaptığı şey , mevcut iş parçacığını depo tamponu boşalıncaya kadar bekletmektir , böylece daha sonraki yüklerimiz , önceki mağazalarımızdan sonra küresel düzende görünür.
(x86 kuvvetle asm bellek modeli araçlarını sipariş etti o volatile
daha yakın size vererek sona erebilir x86 üzerinde mo_acq_rel
hala gerçekleşebilir olmayan atom değişkenlerle yeniden düzenlenmesi o derleme zamanında hariç. Ama en x86 dışındaki bellek modellerini bu yüzden zayıf-emretti volatile
ve relaxed
yaklaşık vardır mo_relaxed
izin verdiği kadar zayıf .)