Sorunun tarihi. C++20'den önce, mevcut atomik olmayan nesnelere atomik işlemlerin uygulanması, std::atomic'in nesnelerin baştan itibaren atomik olarak inşa edilmesini zorunlu kılması nedeniyle zahmetli çözümler gerektiriyordu. Programcılar genellikle düz nesneleri atomik olarak ele almak için tehlikeli reinterpret_cast işlemleri yaparak, katı takas kurallarını ihlal ediyor ve nesne ömrü uyuşmazlıkları nedeniyle belirsiz davranışa yol açıyordu. C++20'de std::atomic_ref'in tanıtılması, nesnelerin depolama türünü veya ömrünü değiştirmeden mevcut nesnelere geçici olarak atomik anlamlar kazandıran sahiplik içermeyen bir görünüm sağlayarak bu açığı kapattı.
Sorun. std::atomic belirli temsil gereksinimleri getiriyor; örneğin, kilit içermeyen bit bayrakları veya dahili mutexler, genellikle nesnenin boyutunu veya hizalamasını, altında yatan tür olan T ile kıyasla değiştiriyor. Dolayısıyla, int türündeki bir nesne std::atomic<int> ile uyumlu bir yerleşim değil, bu da işaretçi dönüştürmeyi imkansız kılıyor. Ayrıca, std::atomic_ref'in referans verilen nesnenin katı hizalama kısıtlamalarını karşılaması gerekiyor; özel olarak, nesnenin adresi en az alignof(std::atomic_ref<T>)'ye hizalanmış olmalı, ki birçok platform için bu alignof(T)'ye eşit ama donanım özel atomik komutlar için daha büyük olabilir. Bu hizalama ön koşulunun ihlal edilmesi belirsiz davranışa yol açar, bu da sıkı mimarilerde (ARM gibi) parçalı okumalar veya donanım istisnaları olarak kendini gösterebilir.
Çözüm. std::atomic_ref, hedef nesneye bir işaretçi tutan hafif bir sarmalayıcı olarak işlev görür, atomikliği sağlamak için derleyici intrinsics veya donanım talimatlarını kullanarak depolamanın bir std::atomic örneği olduğunu varsaymadan atomik işlemleri uygular. Mevcut nesnenin ömrüne saygı gösterirken, her işlem süresince std::atomic ile aynı bellek sıralama garantilerini sağlar. Güvenli bir şekilde kullanmak için geliştiricilerin nesnenin uygun şekilde hizalandığından emin olmaları gerekir, genellikle alignas belirteçleri ile veya std::atomic_ref<T>::required_alignment'ın karşılandığını doğrulayarak, böylelikle eski veri yapıları veya C-uyumlu düzenlere kilitsiz eşzamanlı erişim sağlanmış olur.
#include <atomic> #include <cstdint> #include <iostream> struct alignas(alignof(std::atomic_ref<std::uint64_t>)) Data { std::uint64_t value; }; int main() { Data d{42}; std::atomic_ref<std::uint64_t> ref(d.value); ref.fetch_add(8, std::memory_order_relaxed); std::cout << d.value << " "; // Çıktı: 50 }
Sorun açıklaması. Yüksek frekanslı ticaret uygulamasında, bir miras alınmış C-yapısı, ağ iş parçacığı tarafından atomik güncellemeler gerektiren bir double fiyat alanını içeren piyasa besleme paketlerinin düzenini tanımlıyordu. Borsa, yapının std::atomic<double> kullanacak şekilde değiştirilmesine izin vermediği için tam ikili uyumluluğu zorunlu kıldı ve gecikme gereksinimleri mutex kilitleri veya bellek kopyalarını yasakladı. Parçalı yazmaların neden olduğu verim yarışları ile karşılaştık, bu durumda double (etiketlenmemiş x86-64'de uygun hizalama olmadan) alanındaki kısmi yazmalar, strateji iş parçacığının yüksek volatilite patlamaları sırasında bozulmuş "hayalet" değerleri okumasına sebep oldu.
Düşünülen farklı çözümler. İlk yaklaşım, iki örneği elinde tutarak ve atomik olarak bir işaretçi çevirmek için std::atomic<bool> bayrakları ile çift tamponlama yaptı. Kilitsiz olsa da, bu bellek tüketimini iki katına çıkardı ve NUMA düğümleri arasında önbellek satırı sıçramalarına neden oldu, mikro ölçekte performansı yaklaşık %15 kötüleştirdi. İkinci yaklaşım, yerel bir std::atomic<double> değişkenine std::memcpy yapmayı düşündü, ancak bu, ek kopyalama nedeniyle zamanında kısıtlamaları ihlal ediyordu ve hala parçalı okumalar sorunu yaşıyordu, eğer kopyalama güncelleme sırasında gerçekleşirse. Üçüncü çözüm, fiyat alanını doğrudan C-yapısı içinde referans göstermek için std::atomic_ref kullandı, yapı düzenini değiştirmeden donanım CAS (Compare-And-Swap) talimatlarını kullanarak.
Hangi çözüm seçildi ve neden. std::atomic_ref'i seçtik çünkü gerçek sıfır-overhead soyutlaması sağlıyordu: x86-64 üzerinde üretilen derleme, elle yazılmış lock cmpxchg talimatlarıyla aynıydı, ek tahsisat veya dolaylılık yoktu. Çift tamponlama yaklaşımının aksine, sıcak veriler için tek bir önbellek satırında kalmayı sağladı, mikro saniye seviyesinde gecikme için kritik L1 önbellek yerelitesini korudu. Kritik olarak, dış C kütüphanesinin ABI kısıtlamalarına saygı gösterirken donanım zorlamalı atomiklik ile veri yarışlarını ortadan kaldırdı.
Sonuç. Uygulama sonrası sistem, belirsiz değer anomali sorunlarını ortadan kaldırarak, alt mikro saniye gecikme ile sürekli kilitsiz güncellemeler gerçekleştirdi ve bu durum ThreadSanitizer çalışmalarıyla doğrulandı. Hizalama doğrulaması (alignas), kod değişikliği olmadan ARM64 sunuculara taşınabilirlik sağladı ve çift tamponlama temelinden %12 daha az önbellek baskısı nedeniyle throughput arttı.
Neden bir atomik olmayan işaretçiyi std::atomic<T>* bileşenine dönüştürmek belirsiz davranışı tetiklerken std::atomic_ref güvenlidir?
reinterpret_cast ile yapılmış olan bir dönüştürme, std::atomic<T> türünde bir nesneye işaretçi oluşturur, ancak depolama gerçekte T türünde bir nesne içermektedir. Bu, C++ nesne modelinin katı takas kurallarını ihlal eder ve ömür gereksinimlerini bozarak, çünkü std::atomic<T> mevcut durumda farklı boyut, hizalama veya dahili durum (örneğin, bir spinlock) içerebilir. std::atomic_ref, açıkça bir T nesnesine atıfta bulunan ve bunun üzerinden atomik işlemleri gerçekleştiren ayrı bir referans türü olarak tasarlanmıştır, böylece depolamanın farklı bir tip olduğunu iddia etmeden orijinal nesnenin ömrünü ve düzenini korur.
Kendisine atıfta bulunduğu nesnenin inşasıyla std::atomic_ref senkronize mi?
Hayır. std::atomic_ref sadece üzerinden gerçekleştiren işlemlerde atomiklik sağlar, ancak referans edilen nesnenin yapıcısıyla önce gerçekleşen ilişkileri kurmaz. Eğer A İş Parçacığı bir nesne inşa ederse ve B İş Parçacığı hemen ona bir std::atomic_ref oluşturursa, B İş Parçacığı, A İş Parçacığı bir çıkarım işlemi gerçekleştirmediyse (örneğin, bir std::atomic<bool> içerisine yazmayı) başta başlatılmamış belleği görebilir. atomic_ref kendisi, nesnenin zaten canlı ve erişilebilir olduğunu varsayar, ancak inşa sırasında eşzamanlı atomik olmayan yazmalar dış senkronizasyon olmadan veri yarışlarını sürdürür.
const nesneler ile std::atomic_ref kullanılabilir mi ve sınırlamalar nelerdir?
Evet, std::atomic_ref<const T> geçerlidir ve const olarak tanımlanan nesnelerde atomik okuma işlemlerine (örneğin load) izin verir, şayet nesne, derleyici optimize etmeleriyle değerleri kayıt içinde önbelleğe alacak şekilde const olarak tanımlanmadıysa. Ancak, bir const T&'den bir std::atomic_ref<T> (sabit olmayan) oluşturamazsınız, çünkü bu sabitlik doğruluğunu ihlal eder. Ayrıca, atomic_ref<const T> ile bile, altındaki nesne yazma korumalı bellek (örneğin, .rodata bölümü) içinde olmamalıdır, çünkü donanım atomik talimatları çoğu mimaride okuma işlemleri için yazılabilir önbellek satırları gerektirir.