Programcı düzeyinde C ++ std :: atomic ile neler garanti edilir?


9

Hakkında birkaç makale, görüşme ve yığın akışı ile ilgili soruları dinledim ve okudum ve std::atomicbunu iyi anladığımdan emin olmak istiyorum. Önbellek satırıyla hala kafam karıştığından, MESI (veya türetilmiş) önbellek tutarlılık protokollerinde, mağaza arabelleklerinde, geçersiz sıralarda vb. Olası gecikmeler nedeniyle görünürlük yazıyor.

X86'nın daha güçlü bir bellek modeli olduğunu okudum ve önbellek geçersiz kılma gecikirse x86 başlatılan işlemleri geri alabilir. Ama şimdi sadece platformdan bağımsız olarak bir C ++ programcısı olarak neyi varsaymam gerektiğiyle ilgileniyorum.

[T1: thread1 T2: thread2 V1: paylaşılan atomik değişken]

Anladım ki std :: atomic bunu garanti ediyor,

(1) Bir değişken üzerinde veri yarışları meydana gelmez (önbellek hattına özel erişim sayesinde).

(2) Hangi bellek_sayısını kullandığımıza bağlı olarak, (engellerle) sıralı tutarlılığın (bir engelden önce, bir engelden veya her ikisinden sonra) olmasını garanti eder.

(3) T1 üzerine bir atomik yazımdan (V1) sonra, T2'deki bir atomik RMW (V1) tutarlı olacaktır (önbellek satırı T1'deki yazılı değerle güncellenmiş olacaktır).

Ancak önbellek tutarlılık primerinden bahsedildiği gibi,

Tüm bunların anlamı, varsayılan olarak yüklerin eski verileri getirebilmesidir (karşılık gelen bir geçersiz kılma isteği geçersiz kılma kuyruğunda oturuyorsa)

Peki, aşağıdakiler doğru mu?

(4) std::atomic, T2'nin T1 üzerine bir atomik yazıdan (V) sonra bir atomik okuma (V) üzerinde 'bayat' bir değer okumadığını garanti ETMEZ.

(4) doğruysa sorular: T1'deki atomik yazma gecikme ne olursa olsun önbellek satırını geçersiz kılarsa, T2 bir atomik RMW işlemi yaparken ancak bir atomik okumada geçersiz kılmanın etkili olmasını neden bekler?

(4) yanlışsa sorular: bir iş parçacığı yürütmede "bayat" değerini ve "görünür" ne zaman okuyabilir?

Cevaplarını çok takdir ediyorum

Güncelleme 1

Görünüşe göre (3) 'de yanılmışım. İlk V1 = 0 için aşağıdaki serpiştirmeyi düşünün:

T1: W(1)
T2:      R(0) M(++) W(1)

Bu durumda T2'nin RMW'sinin tamamen W (1) 'den sonra gerçekleşeceği garanti edilmesine rağmen, yine de' eski 'bir değer okuyabilir (yanılmışım). Buna göre, atomik tam önbellek tutarlılığını garanti etmez, sadece sıralı tutarlılığı garanti eder.

Güncelleme 2

(5) Şimdi bu örneği düşünün (x = y = 0 ve atomik):

T1: x = 1;
T2: y = 1;
T3: if (x==1 && y==0) print("msg");

konuştuklarımıza göre, ekranda görüntülenen "msg" yi görmek bize T2'den sonra T2'nin yürütülmesinin ötesinde bilgi vermeyecektir. Yani aşağıdaki infazlardan herhangi biri olmuş olabilir:

  • T1 <T3 <T2
  • T1 <T2 <T3 (burada T3 x = 1 görür ancak y = 1 görmez)

bu doğru mu?

(6) Bir iş parçacığı her zaman 'eski' değerleri okuyabiliyorsa, tipik "yayınla" senaryosunu alırsak ancak bazı verilerin hazır olduğunu belirtmek yerine tam tersini yaparız (verileri silin)?

T1: delete gameObjectPtr; is_enabled.store(false, std::memory_order_release);
T2: while (is_enabled.load(std::memory_order_acquire)) gameObjectPtr->doSomething();

burada T2, is_enabled'ın yanlış olduğunu görünceye kadar silinmiş bir ptr kullanır.

(7) Ayrıca, ipliklerin 'bayat' değerleri okuyabileceği gerçeği, muteksin sadece bir kilitsiz atom hakkı ile uygulanamayacağı anlamına mı geliyor? Dişler arasında bir senkron mekanizması gerektirir. Kilitlenebilir bir atomik mi gerektirir?

Yanıtlar:


3
  1. Evet, veri yarışı yok
  2. Evet, uygun memory_orderdeğerlerle sıralı tutarlılığı garanti edebilirsiniz
  3. Bir atomik okuma-değiştirme-yazma işlemi daima aynı değişkene bir atomik yazma işleminden önce veya tamamen sonra gerçekleşir
  4. Evet, T2, T1'e atomik bir yazma işleminden sonra bir değişkenten eski değeri okuyabilir

Atomik okuma-değiştirme-yazma işlemleri atomikliklerini garanti edecek şekilde belirtilmiştir. İlk okumadan sonra ve bir RMW işleminin yazılmasından önce başka bir iş parçacığı değere yazabiliyorsa, bu işlem atomik olmaz.

İş parçacıkları her zaman eski değerleri okuyabilir, ancak öncesinde olanlar göreceli sıralamayı garanti eder .

Bir RMW işlemi bir "eski" değer okursa, oluşturduğu yazmanın, okuduğu değerin üzerine yazacak diğer iş parçacıklarından herhangi bir yazma işleminden önce görünür olacağını garanti eder.

Örneğin güncelleme

T1 yazar ise x=1ve T2 yapar x++ile, xilk olarak 0, depolanması açısından seçenekler xvardır:

  1. İlk olarak T1'in yazımıdır, bu nedenle T1 yazar x=1, sonra T2 okur x==1, bunu 2'ye çıkarır ve x=2tek bir atomik işlem olarak geri yazar .

  2. T1'in yazımı ikinci. T2 okur x==0, 1'e yükseltir ve x=1tek bir işlem olarak geri yazar , sonra T1 yazar x=1.

Ancak, bu iki iş parçacığı arasında başka senkronizasyon noktası yoksa, iş parçacıkları belleğe boşaltılamayan işlemlerle devam edebilir.

Bu nedenle, T1, x=1T2 hala okuyacak x==0(ve dolayısıyla yazacak x=1) olsa da, başka şeylerle devam edebilir .

Eğer başka bir senkronizasyon noktası varsa, o zaman xönce hangi iş parçacığının değiştirildiği anlaşılacaktır , çünkü bu senkronizasyon noktaları bir emri zorlayacaktır.

Bu, bir RMW işleminden okunan değer üzerinde bir koşulunuz varsa en belirgindir.

Güncelleme 2

  1. memory_order_seq_cstTüm atomik işlemler için (varsayılan) kullanırsanız , bu tür şeyler için endişelenmenize gerek yoktur. Programın bakış açısından, eğer "msg" görürseniz T1, sonra T3, sonra T2 koştu.

Başka bellek sıralamaları (özellikle memory_order_relaxed) kullanıyorsanız, kodunuzda başka senaryolar görebilirsiniz.

  1. Bu durumda, bir hatanız var. is_enabledT2 whiledöngüsüne girdiğinde bayrağın doğru olduğunu varsayalım , böylece gövdeyi çalıştırmaya karar verir. T1 artık verileri siler ve ardından T2, sarkan bir işaretçi olan işaretçiyi erteler ve tanımlanmamış davranış ortaya çıkar. Atomlar, bayrak üzerindeki veri yarışını engellemenin ötesinde herhangi bir şekilde yardım etmez veya engellemez.

  2. Sen olabilir tek atom değişkeni bulunan muteksi uygulamak.


Hızlı cevabınız için @Anthony Wiliams'a çok teşekkürler. Sorumumu bir 'bayat' değeri okuma RMW örneği ile güncelledim. Bu örneğe baktığımızda, göreli sıralamayla ne demek istiyorsun ve herhangi bir yazmadan önce T2'nin W (1) 'i görünür olacak mı? Bu, T2'nin T1'in değişikliklerini gördüğü zaman, artık T2'nin W (1) 'i okumadığını mı gösteriyor?
Albert Caldas

Bu nedenle "İş parçacıkları her zaman eski değerleri okuyabilirse", önbellek tutarlılığının asla garanti edilmediği anlamına gelir (en azından c ++ programcı düzeyinde). Güncellememe2 bakabilir misiniz lütfen?
Albert Caldas

Şimdi görüyorum ki, tüm bunları tam olarak anlamak için dil ve donanım belleği modellerine daha fazla dikkat etmeliydim, bu benim eksik olduğum parçaydı. çok teşekkürler!
Albert Caldas

1

(3) ile ilgili olarak - kullanılan bellek sırasına bağlıdır. Hem mağaza hem de RMW işlemi kullanılıyorsa std::memory_order_seq_cst, her iki işlem de bir şekilde sipariş edilir - yani, mağaza RMW'den önce veya başka bir şekilde gerçekleşir. Mağaza, RMW'den önce sipariş edilirse, RMW işleminin depolanan değeri "gördüğü" garanti edilir. RMW'den sonra mağaza sipariş edilirse, RMW işlemi tarafından yazılan değerin üzerine yazılır.

Daha rahat bellek siparişleri kullanırsanız, değişiklikler yine de bir şekilde sipariş edilecektir (değişkenin değişiklik sırası), ancak RMW'nin mağaza işleminden değeri "görüp görmediği" - RMW işlemi olsa bile düzendir sonra değişkenin modifikasyon sırayla yazma.

Başka bir makaleyi okumak isterseniz, sizi C / C ++ Programcıları için Bellek Modelleri'ne yönlendirebilirim .


Makale için teşekkürler, henüz okumamıştım. Oldukça eski olsa bile, fikirlerimi bir araya getirmek yararlı oldu.
Albert Caldas

1
Bunu duyduğuma sevindim - bu makale yüksek lisans tezimden biraz genişletilmiş ve gözden geçirilmiş bir bölüm. :-) C ++ 11 tanıtıldığı gibi bellek modeline odaklanır; C ++ 14 / 17'de sunulan (küçük) değişiklikleri yansıtacak şekilde güncelleyebilirim. İyileştirmeler için herhangi bir yorumunuz veya öneriniz varsa lütfen bize bildirin!
mpoeter
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.