Klasik bir sorunun bir varyantını çözen taşınabilir kod (Intel, ARM, PowerPC ...) yazmak istiyorum:
Initially: X=Y=0
Thread A:
X=1
if(!Y){ do something }
Thread B:
Y=1
if(!X){ do something }
hangi amaç hem ipler yapıyoruz bir durum kaçınmaktırsomething
. (Hiçbir şey çalışmazsa sorun değil, bu tam olarak bir kez çalıştırılan bir mekanizma değildir.) Aşağıdaki gerekçemde bazı kusurlar görüyorsanız lütfen beni düzeltin.
memory_order_seq_cst
Atomik store
s ve load
s ile hedefe şu şekilde ulaşabileceğimin farkındayım :
std::atomic<int> x{0},y{0};
void thread_a(){
x.store(1);
if(!y.load()) foo();
}
void thread_b(){
y.store(1);
if(!x.load()) bar();
}
Bu, hedefe ulaşır, çünkü
{x.store(1), y.store(1), y.load(), x.load()}
etkinliklerde program sırası "kenarları" ile anlaşması gereken tek bir toplam sipariş olmalıdır:
x.store(1)
"TO içinde önce"y.load()
y.store(1)
"TO içinde önce"x.load()
ve foo()
çağrıldıysa, ek avantajımız var:
y.load()
"değeri daha önce okur"y.store(1)
ve bar()
çağrıldıysa, ek avantajımız var:
x.load()
"değeri daha önce okur"x.store(1)
ve tüm bu kenarların bir araya getirilmesi bir döngü oluşturur:
x.store(1)
"içindeki TO daha önce" y.load()
"değeri" y.store(1)
"den önce TO'da" "daha önce x.load()
değer okuyor"x.store(true)
bu da emirlerin hiçbir döngüsünün olmaması gerçeğini ihlal ediyor.
Kasıtlı olarak standart olmayan terimleri "TO önce" ve "önceki değeri okur" gibi standart terimlerin aksine kullanıyorum happens-before
, çünkü bu kenarların gerçekten bir happens-before
ilişki anlamına geldiği varsayımımın doğruluğu hakkında geri bildirim istiyorum. ve bu tür birleşik grafikteki döngü yasaktır. Ben bu konuda emin değilim. Ne biliyorum bu kod Intel gcc & clang ve ARM gcc doğru engelleri üretir
Şimdi, gerçek sorunum biraz daha karmaşık, çünkü "X" üzerinde hiçbir kontrole sahip değilim - bazı makroların, şablonların vb. Arkasında gizlenmiş ve daha zayıf olabilir. seq_cst
"X" in tek bir değişken mi yoksa başka bir kavram mı olduğunu bilmiyorum (örneğin hafif semafor veya muteks). Tüm bildiğim iki makro var set()
ve check()
böyle "sonra" başka bir iş parçacığı çağırdı check()
döner öyle . (O olduğu da bilinmektedir ve evreli vardır ve veri yarış UB oluşturamazsınız.)true
set()
set
check
Yani kavramsal set()
olarak "X = 1" check()
gibi ve "X" gibi, ama varsa atomiklere doğrudan erişimim yok.
void thread_a(){
set();
if(!y.load()) foo();
}
void thread_b(){
y.store(1);
if(!check()) bar();
}
Endişeliyim, bu set()
dahili olarak uygulanabilir x.store(1,std::memory_order_release)
ve / veya check()
olabilir x.load(std::memory_order_acquire)
. Ya da varsayımsal olarak bir std::mutex
iş parçacığının kilidinin açılması ve bir iş parçacığının açılması try_lock
; ISO standardında std::mutex
sadece seq_cst değil, sipariş alma ve bırakma garantisi vardır.
Eğer durum buysa, o zaman check()
beden daha önce "yeniden sıralanabilir" ise y.store(true)
( Alex'in PowerPC'de bunun olduğunu gösterdikleri cevabına bakınız ).
Şimdi bu olaylar dizisi mümkün olduğu için bu gerçekten kötü olurdu:
thread_b()
öncex
(0
) eski değerini yüklerthread_a()
dahil her şeyi yürütürfoo()
thread_b()
dahil her şeyi yürütürbar()
Yani, hem foo()
ve bar()
ben önlemek zorunda olan çağırıldım. Bunu önlemek için seçeneklerim nelerdir?
Seçenek A
Depo Yük bariyerini zorlamaya çalışın. Bu, pratikte, Alexstd::atomic_thread_fence(std::memory_order_seq_cst);
tarafından farklı bir cevapta açıklandığı gibi, test edilen tüm derleyiciler tam bir çit yayınladı:
- x86_64: MFENCE
- PowerPC: hwsync
- Itanuim: mf
- ARMv7 / ARMv8: dmb ish
- MIPS64: senkronizasyon
Bu yaklaşımla ilgili sorun, std::atomic_thread_fence(std::memory_order_seq_cst)
tam bellek bariyerine çevirmek zorunda C ++ kurallarında herhangi bir garanti bulamadı olmasıdır . Aslında, atomic_thread_fence
C ++ 'daki s kavramı, bellek bariyerlerinin montaj konseptinden farklı bir soyutlama düzeyinde görünmekte ve "atomik işlemin neyle senkronize olduğu" gibi şeylerle daha fazla ilgilenmektedir. Aşağıdaki uygulamanın hedefe ulaştığına dair herhangi bir teorik kanıt var mı?
void thread_a(){
set();
std::atomic_thread_fence(std::memory_order_seq_cst)
if(!y.load()) foo();
}
void thread_b(){
y.store(true);
std::atomic_thread_fence(std::memory_order_seq_cst)
if(!check()) bar();
}
Seçenek B
Y üzerinde okuma-değiştirme-yazma memory_order_acq_rel işlemlerini kullanarak senkronizasyonu elde etmek için Y üzerinde yaptığımız kontrolü kullanın:
void thread_a(){
set();
if(!y.fetch_add(0,std::memory_order_acq_rel)) foo();
}
void thread_b(){
y.exchange(1,std::memory_order_acq_rel);
if(!check()) bar();
}
Buradaki fikir, tek atom için erişimler (yani y
böylece ya) Tüm gözlemciler üzerinde uzlaştıkları tek bir sipariş formu olmalı fetch_add
öncedir exchange
veya tersi.
Eğer fetch_add
daha önce exchange
ise "release" kısmı fetch_add
ile "acquire" kısmı ile senkronize edilir exchange
ve böylece tüm yan etkileri set()
kod yürütme görünür olması gerekir check()
, bu yüzden bar()
çağrılmaz.
Aksi takdirde, exchange
önce fetch_add
, o fetch_add
zaman görecek 1
ve aramayacak foo()
. Yani, her iki çağırmak mümkün değildir foo()
ve bar()
. Bu muhakeme doğru mu?
Seçenek C
Afet önlemek "kenarları" tanıtmak için kukla atomlar kullanın. Aşağıdaki yaklaşımı düşünün:
void thread_a(){
std::atomic<int> dummy1{};
set();
dummy1.store(13);
if(!y.load()) foo();
}
void thread_b(){
std::atomic<int> dummy2{};
y.store(1);
dummy2.load();
if(!check()) bar();
}
Buradaki sorunun atomic
yerel olduğunu düşünüyorsanız, bunları küresel kapsama taşıdığınızı hayal edin, aşağıdaki muhakemede benim için önemli görünmüyor ve kasten bu kukla ne kadar komik olduğunu ortaya çıkaracak şekilde kodu yazdım1 ve dummy2 tamamen ayrıdır.
Neden Dünya'da bu işe yarayabilir? Peki, toplam sırası {dummy1.store(13), y.load(), y.store(1), dummy2.load()}
program sırası "kenarları" ile tutarlı olması gereken tek bir toplam sırası olmalıdır :
dummy1.store(13)
"TO içinde önce"y.load()
y.store(1)
"TO içinde önce"dummy2.load()
(Bir seq_cst deposu + yükü, ayrı bir engelleme talimatının gerekli olmadığı AArch64 bile dahil gerçek ISA'larda olduğu gibi, StoreLoad dahil tam bir bellek bariyerinin C ++ eşdeğerini umarım oluşturur.)
Şimdi, dikkate almamız gereken iki durum var: ya toplam sırayla y.store(1)
önce y.load()
ya da sonra.
Eğer y.store(1)
öncedir y.load()
sonra foo()
aradı ve güvenli olmayacaktır.
Daha y.load()
önce ise y.store(1)
, daha önce program düzeninde bulunan iki kenarla birleştirerek şunu ortaya çıkarırız:
dummy1.store(13)
"TO içinde önce"dummy2.load()
Şimdi, dummy1.store(13)
etkilerini serbest bırakan set()
ve dummy2.load()
bir edinme işlemidir, bu yüzden check()
etkilerini görmeli set()
ve böylece bar()
çağrılmayacak ve güvende olacağız.
Bunun check()
sonuçlarını göreceğini düşünmek doğru set()
mu? Çeşitli "kenarları" ("program sırası" aka Sıralı Önce, "toplam sipariş", "yayınlamadan önce", "edinme sonra") bu şekilde birleştirebilir miyim? Bu konuda ciddi şüphelerim var: C ++ kuralları aynı konumda mağaza ve yük arasındaki ilişkilerle "senkronize" ilişkileri hakkında konuşmak gibi görünüyor - burada böyle bir durum yok.
Biz sadece dava hakkında endişeli olduğunuzu Not dumm1.store
edilmektedir bilinen (diğer akıl yoluyla) önce olmak dummy2.load
seq_cst toplam sipariş. Dolayısıyla, aynı değişkene erişiyor olsaydı, yük saklanan değeri görür ve onunla senkronize olur.
(Atomik yüklerin ve depoların en az 1 yollu bellek bariyerlerine derlendiği (ve seq_cst işlemlerinin yeniden sıralanamayacağı uygulamalar için bellek bariyeri / yeniden sıralama nedeni: örneğin bir seq_cst deposu bir seq_cst yükünü geçemez) mağazalar sonra dummy2.load
kesinlikle başka iş parçacığı tarafından görülebilir hale sonrasında y.store
. Ve benzer diğer iş, ... önce y.load
.)
Https://godbolt.org/z/u3dTa8 adresinden Seçenekler A, B, C uygulamamla oynayabilirsiniz.
foo()
ve önleyin bar()
.
compare_exchange_*
Bir atomik bool üzerinde değerini değiştirmeden RMW işlemi yapmak için kullanabilirsiniz (sadece beklenen ve yeni değeri aynı değere ayarlayın).
atomic<bool>
vardır exchange
ve compare_exchange_weak
. İkincisi, CAS (doğru, doğru) veya yanlış, yanlış (denemeye) çalışarak bir sahte RMW yapmak için kullanılabilir. Başarısız olur veya değeri atomla değiştirir. (X86-64 asm'da, bu hile, lock cmpxchg16b
garantili atomik 16 bayt yükleri nasıl yaptığınızdır; verimsiz ama ayrı bir kilit almaktan daha az kötü.)
foo()
de ne bar()
denir olacağını biliyorum . Ben "sorun X var ama Y sorun var" tür cevaplar önlemek için, kodun birçok "gerçek dünya" unsurları getirmek istemiyordu. Ancak, eğer gerçekten arka plan katının ne olduğunu bilmek gerekiyorsa: set()
gerçekten some_mutex_exit()
, check()
öyle try_enter_some_mutex()
, y
"bazı garsonlar var", foo()
"kimseyi uyandırmadan çık", bar()
"uyanmayı bekle" ... Ama reddediyorum Bu tasarımı burada tartış - gerçekten değiştiremem.
std::atomic_thread_fence(std::memory_order_seq_cst)
tam bir bariyere derleme yapar, ancak tüm konsept bir uygulama ayrıntı olduğundan size bulamazsınız standart herhangi bir söz. (CPU bellek modeller genellikle edilir reorerings sıralı kıvama göre izin verilen ne açısından tanımlandığı gibi 86 olan seq sabit + bir mağaza tamponu ağırlık / yönlendirme.)