Std :: atomic tam olarak nedir?


174

Bunun std::atomic<>atomik bir nesne olduğunu anlıyorum . Ama ne ölçüde atomik? Anladığım kadarıyla bir işlem atomik olabilir. Bir nesneyi atom haline getirerek tam olarak ne kastedilmektedir? Örneğin, aşağıdaki kodu aynı anda yürüten iki iş parçacığı varsa:

a = a + 12;

O zaman tüm operasyon add_twelve_to(int)atomik midir? Veya atomik (yani operator=()) değişkeninde değişiklikler yapıldı mı?


9
a.fetch_add(12)Atomik bir RMW gibi bir şey kullanmanız gerekir .
Kerrek SB

Evet bunu anlamadım. Bir nesneyi atom yapmakla kastedilen. Bir arayüz olsaydı, muteks veya monitör ile atomik hale getirilebilirdi.

2
@AaryamanSagar bir verimlilik sorununu çözüyor. Muteksler ve monitörler hesaplama yükünü taşır. Kullanarak std::atomicstandart kütüphane atomisiteye ulaşmak için neye ihtiyaç duyduğuna karar verir.
Drew Dormann

1
@AaryamanSagar: Atomik işlemlere izin verenstd::atomic<T> bir tiptir . Sihirli bir şekilde hayatınızı iyileştirmez, yine de onunla ne yapmak istediğinizi bilmeniz gerekir. Çok özel bir kullanım durumu içindir ve atomik işlemlerin (nesne üzerinde) kullanımı genellikle çok incedir ve yerel olmayan bir perspektiften düşünülmesi gerekir. Bunu zaten bilmiyorsanız ve neden atomik operasyonlar istemiyorsanız, tür muhtemelen sizin için çok fazla kullanılmaz.
Kerrek SB

Yanıtlar:


188

Std :: atomic <> öğesinin her bir somutlaştırılması ve tam uzmanlaşması, tanımlanmamış davranışı yükseltmeden farklı iş parçacıklarının aynı anda çalışabileceği (örnekleri) bir türü temsil eder:

Atomik tipteki nesneler, veri yarışlarından arınmış tek C ++ nesneleridir; yani, bir iş parçacığı atomik bir nesneye başka bir iş parçacığı okurken yazarsa, davranış iyi tanımlanır.

Ek olarak, atomik nesnelere erişim, iş parçacıkları arası senkronizasyon oluşturabilir ve ile belirtildiği gibi atom olmayan bellek erişimlerini sıralayabilir std::memory_order.

std::atomic<>C ++ öncesi 11 kez, GCC durumunda MSVC veya atomik bultinler ile (örneğin) kilitli fonksiyonlar kullanılarak yapılması gereken işlemleri sarar .

Ayrıca, senkronizasyon ve sipariş kısıtlamalarını belirten std::atomic<>çeşitli bellek siparişlerine izin vererek size daha fazla kontrol sağlar . C ++ 11 atomları ve bellek modeli hakkında daha fazla okumak istiyorsanız, bu bağlantılar faydalı olabilir:

Tipik kullanım durumlarında, muhtemelen aşırı yüklenmiş aritmetik işleçler veya bunların başka bir setini kullanacağınızı unutmayın :

std::atomic<long> value(0);
value++; //This is an atomic op
value += 5; //And so is this

Operatör sözdizimi bellek sırasını belirtmenize izin vermediğinden, bu işlemler std::memory_order_seq_cstC ++ 11'deki tüm atomik işlemler için varsayılan sipariş olduğundan bu işlemlerle gerçekleştirilecektir . Tüm atomik işlemler arasındaki sıralı tutarlılığı (toplam küresel sıralama) garanti eder.

Bununla birlikte, bazı durumlarda, bu gerekli olmayabilir (ve hiçbir şey ücretsiz değildir), bu nedenle daha açık bir form kullanmak isteyebilirsiniz:

std::atomic<long> value {0};
value.fetch_add(1, std::memory_order_relaxed); // Atomic, but there are no synchronization or ordering constraints
value.fetch_add(5, std::memory_order_release); // Atomic, performs 'release' operation

Şimdi, örneğiniz:

a = a + 12;

tek bir atomik op için değerlendirilmeyecektir: sonuçta a.load()(atomun kendisidir), daha sonra bu değer 12ile a.store()( ve ayrıca atomik) nihai sonucun eklenmesiyle sonuçlanacaktır. Daha önce de belirttiğim gibi, std::memory_order_seq_cstburada kullanılacaktır.

Ancak, yazarsanız a += 12, atomik bir işlem olacaktır (daha önce belirttiğim gibi) ve kabaca eşdeğerdir a.fetch_add(12, std::memory_order_seq_cst).

Yorumunuza gelince:

Düzenli intatomik yükler ve mağaza bulunuyor. Sarmanın anlamı atomic<>nedir?

İfadeniz yalnızca depolar ve / veya yükler için böyle bir atomisite garantisi sağlayan mimariler için geçerlidir. Bunu yapmayan mimariler var. Ayrıca, genellikle atomik olmak için sözcük / kelime ile hizalanmış adres üzerinde işlemlerin yapılması zorunludur ve ek gereklilikler olmaksızın her platformda std::atomic<>atomik olması garanti edilen bir şeydir . Ayrıca, böyle bir kod yazmanıza izin verir:

void* sharedData = nullptr;
std::atomic<int> ready_flag = 0;

// Thread 1
void produce()
{
    sharedData = generateData();
    ready_flag.store(1, std::memory_order_release);
}

// Thread 2
void consume()
{
    while (ready_flag.load(std::memory_order_acquire) == 0)
    {
        std::this_thread::yield();
    }

    assert(sharedData != nullptr); // will never trigger
    processData(sharedData);
}

Onaylama koşulunun her zaman doğru olacağını (ve dolayısıyla hiçbir zaman tetiklenmeyeceğini) unutmayın, böylece whiledöngü çıktıktan sonra verilerin hazır olduğundan her zaman emin olabilirsiniz . Çünkü bu:

  • store()işareti sharedDataayarlandıktan sonra gerçekleştirilir ( generateData()her zaman yararlı bir şey döndürdüğünü, özellikle de asla döndürmediğini varsayarız NULL) ve std::memory_order_releasesipariş kullanır :

memory_order_release

Bu bellek sırasına sahip bir depolama işlemi, serbest bırakma işlemini gerçekleştirir : bu iş parçacığından sonra geçerli iş parçacığında okuma veya yazma işlemi yeniden sıralanamaz . Geçerli iş parçacığındaki tüm yazma işlemleri, aynı atom değişkenini elde eden diğer iş parçacıklarında görülebilir

  • sharedDatawhiledöngü çıktıktan sonra kullanılır ve böylece load()bayraktan sonra sıfırdan farklı bir değer döndürür. sipariş load()kullanır std::memory_order_acquire:

std::memory_order_acquire

Bu bellek sırasına sahip bir yükleme işlemi , etkilenen bellek konumunda alma işlemini gerçekleştirir : bu iş parçacığından önce geçerli iş parçacığında okuma veya yazma işlemi yeniden sıralanamaz . Aynı atom değişkenini serbest bırakan diğer evrelerdeki tüm yazılar geçerli evre içinde görülebilir .

Bu, senkronizasyon üzerinde kesin kontrol sağlar ve kodunuzun nasıl davranabileceğini / olmayacağını / davranmayacağını açıkça belirtmenizi sağlar. Atomikliğin kendisi sadece garanti olsaydı bu mümkün olmazdı. Özellikle yayın-tüketme siparişi gibi çok ilginç senkronizasyon modelleri söz konusu olduğunda .


2
Aslında atomik yükleri ve ints gibi ilkeller için depoları olmayan mimariler var mı?

7
Sadece atomisite ile ilgili değil. aynı zamanda sipariş, çok çekirdekli sistemlerde davranış vb . ile ilgilidir. Bu makaleyi okumak isteyebilirsiniz .
Mateusz Grzejek

4
@AaryamanSagar Eğer yanılmıyorsam, x86 okuyup yazarken bile SADECE kelime sınırlarına göre hizalanmışsa atomiktir.
v.shashenko

@MateuszGrzejek Atom tipine referans aldım. Aşağıdakilerin nesne ataması üzerinde hala atomik çalışmayı garanti edip etmeyeceğini lütfen kontrol edebilir misiniz ideone.com/HpSwqo
xAditya3393

3
@TimMB Evet, normal olarak, yürütme sırasının değiştirilebileceği (en azından) iki durumunuz olur: (1) derleyici, çıktı kodunun daha iyi performans göstermesini sağlamak için talimatları yeniden sıralayabilir (standart izin verdiği ölçüde) (CPU kayıtlarının, tahminlerin vb. kullanımına dayalı olarak) ve (2) CPU, örneğin önbellek senkronizasyon noktalarının sayısını en aza indirmek için talimatları farklı bir sırayla yürütebilir. std::atomic( std::memory_order) İçin verilen sipariş kısıtlamaları , tam olarak gerçekleşmesine izin verilen yeniden siparişleri sınırlama amacına hizmet eder.
Mateusz Grzejek

20

Bunun std::atomic<>bir nesneyi atom haline getirdiğini anlıyorum .

Bu bir perspektif meselesidir ... onu keyfi nesnelere uygulayamazsınız ve işlemlerinin atom haline gelmesini sağlayabilirsiniz, ancak (çoğu) integral türleri ve işaretçiler için sağlanan uzmanlıklar kullanılabilir.

a = a + 12;

std::atomic<>değil (kullanım şablon ifadeler için), tek bir atom operasyon için bu basitleştirmek gelmez yerine operator T() const volatile noexceptüye atom yapar load()ait aon iki ilave edilir, ve operator=(T t) noexceptbir yapar store(t).


Sormak istediğim buydu. Düzenli bir int atomik yüklere ve depolara sahiptir. Ne atom ile sarma noktası <>

8
@AaryamanSagar Basit bir intşekilde değişiklik yapmak, değişikliğin diğer iş parçacıklarından görünür olmasını portatif olarak sağlamaz ya da okumak, diğer iş parçacıklarının değişikliklerini görmenizi sağlamaz ve kullanmadıkça bazı şeylerin my_int += 3atomik olarak yapılması garanti edilmez std::atomic<>- bunlar içerebilir aynı değeri güncellemeye çalışan başka bir iş parçacığı getirme işleminden sonra ve depodan önce gelebilir ve iş parçacığınızın güncellemesini hızlandırabilir.
Tony Delroy

" Normal bir intu basitçe değiştirmek, değişikliğin diğer iş parçacıklarından görülebilmesini garanti etmez " Bundan daha kötü: Bu görünürlüğü ölçmek için yapılan herhangi bir girişim, UB ile sonuçlanacaktır.
curiousguy

8

std::atomic çünkü birçok ISA'nın doğrudan donanım desteği var

C ++ standardının söyledikleri std::atomicdiğer cevaplarda analiz edildi.

Şimdi std::atomicfarklı bir fikir edinmek için neyin derlendiğini görelim .

Bu deneyden elde edilen temel paket, modern CPU'ların x86'daki LOCK öneki gibi atomik tamsayı işlemleri için doğrudan desteğe sahip olması ve std::atomictemelde bu talimatlar için taşınabilir bir arayüz olarak var olmasıdır: "Kilit" talimatı x86 montajında ​​ne anlama geliyor? Aarch64'te LDADD kullanılır.

Bu destek gibi daha genel yöntemlere daha hızlı alternatifler sağlar std::mutexdaha yavaş olma pahasına, daha karmaşık çok talimat bölümleri atomik yapabilirsiniz std::atomicçünkü std::mutexo yapar futexyavaş yaydığı userland talimatları daha yoludur Linux sistem çağrıları, std::atomic, ayrıca bkz: std :: mutex bir çit oluşturur mu?

Hangi önişlemcinin tanımlandığına bağlı olarak farklı senkronizasyon mekanizmalarıyla, küresel bir değişkeni birden çok iş parçacığında artıran aşağıdaki çok iş parçacıklı programı ele alalım.

main.cpp

#include <atomic>
#include <iostream>
#include <thread>
#include <vector>

size_t niters;

#if STD_ATOMIC
std::atomic_ulong global(0);
#else
uint64_t global = 0;
#endif

void threadMain() {
    for (size_t i = 0; i < niters; ++i) {
#if LOCK
        __asm__ __volatile__ (
            "lock incq %0;"
            : "+m" (global),
              "+g" (i) // to prevent loop unrolling
            :
            :
        );
#else
        __asm__ __volatile__ (
            ""
            : "+g" (i) // to prevent he loop from being optimized to a single add
            : "g" (global)
            :
        );
        global++;
#endif
    }
}

int main(int argc, char **argv) {
    size_t nthreads;
    if (argc > 1) {
        nthreads = std::stoull(argv[1], NULL, 0);
    } else {
        nthreads = 2;
    }
    if (argc > 2) {
        niters = std::stoull(argv[2], NULL, 0);
    } else {
        niters = 10;
    }
    std::vector<std::thread> threads(nthreads);
    for (size_t i = 0; i < nthreads; ++i)
        threads[i] = std::thread(threadMain);
    for (size_t i = 0; i < nthreads; ++i)
        threads[i].join();
    uint64_t expect = nthreads * niters;
    std::cout << "expect " << expect << std::endl;
    std::cout << "global " << global << std::endl;
}

GitHub akış yukarı .

Derleyin, çalıştırın ve sökün:

comon="-ggdb3 -O3 -std=c++11 -Wall -Wextra -pedantic main.cpp -pthread"
g++ -o main_fail.out                    $common
g++ -o main_std_atomic.out -DSTD_ATOMIC $common
g++ -o main_lock.out       -DLOCK       $common

./main_fail.out       4 100000
./main_std_atomic.out 4 100000
./main_lock.out       4 100000

gdb -batch -ex "disassemble threadMain" main_fail.out
gdb -batch -ex "disassemble threadMain" main_std_atomic.out
gdb -batch -ex "disassemble threadMain" main_lock.out

Aşağıdakiler için son derece muhtemel "yanlış" yarış koşulu çıktısı main_fail.out:

expect 400000
global 100000

ve diğerlerinin deterministik "doğru" çıktısı:

expect 400000
global 400000

Demontajı main_fail.out:

   0x0000000000002780 <+0>:     endbr64 
   0x0000000000002784 <+4>:     mov    0x29b5(%rip),%rcx        # 0x5140 <niters>
   0x000000000000278b <+11>:    test   %rcx,%rcx
   0x000000000000278e <+14>:    je     0x27b4 <threadMain()+52>
   0x0000000000002790 <+16>:    mov    0x29a1(%rip),%rdx        # 0x5138 <global>
   0x0000000000002797 <+23>:    xor    %eax,%eax
   0x0000000000002799 <+25>:    nopl   0x0(%rax)
   0x00000000000027a0 <+32>:    add    $0x1,%rax
   0x00000000000027a4 <+36>:    add    $0x1,%rdx
   0x00000000000027a8 <+40>:    cmp    %rcx,%rax
   0x00000000000027ab <+43>:    jb     0x27a0 <threadMain()+32>
   0x00000000000027ad <+45>:    mov    %rdx,0x2984(%rip)        # 0x5138 <global>
   0x00000000000027b4 <+52>:    retq

Demontajı main_std_atomic.out:

   0x0000000000002780 <+0>:     endbr64 
   0x0000000000002784 <+4>:     cmpq   $0x0,0x29b4(%rip)        # 0x5140 <niters>
   0x000000000000278c <+12>:    je     0x27a6 <threadMain()+38>
   0x000000000000278e <+14>:    xor    %eax,%eax
   0x0000000000002790 <+16>:    lock addq $0x1,0x299f(%rip)        # 0x5138 <global>
   0x0000000000002799 <+25>:    add    $0x1,%rax
   0x000000000000279d <+29>:    cmp    %rax,0x299c(%rip)        # 0x5140 <niters>
   0x00000000000027a4 <+36>:    ja     0x2790 <threadMain()+16>
   0x00000000000027a6 <+38>:    retq   

Demontajı main_lock.out:

Dump of assembler code for function threadMain():
   0x0000000000002780 <+0>:     endbr64 
   0x0000000000002784 <+4>:     cmpq   $0x0,0x29b4(%rip)        # 0x5140 <niters>
   0x000000000000278c <+12>:    je     0x27a5 <threadMain()+37>
   0x000000000000278e <+14>:    xor    %eax,%eax
   0x0000000000002790 <+16>:    lock incq 0x29a0(%rip)        # 0x5138 <global>
   0x0000000000002798 <+24>:    add    $0x1,%rax
   0x000000000000279c <+28>:    cmp    %rax,0x299d(%rip)        # 0x5140 <niters>
   0x00000000000027a3 <+35>:    ja     0x2790 <threadMain()+16>
   0x00000000000027a5 <+37>:    retq

Sonuç:

  • atomik olmayan versiyon genel olanı bir sicile kaydeder ve sicili arttırır.

    Bu nedenle, sonunda, büyük olasılıkla dört yazı aynı "yanlış" değeriyle küreselleşmektedir 100000.

  • std::atomicderler lock addq. LOCK öneki aşağıdaki incbelleği atomik olarak getirir, değiştirir ve günceller.

  • açık satır içi derleme KİLİT önekimiz, hemen hemen aynı şeyle derlenir; bunun yerine, std::atomicbizim incyerine kullanılır add. GCC'nin addkod çözme 1 bayt daha küçük ürettiğini düşünerek neden GCC'yi seçtiğinden emin değilim .

ARMv8, daha yeni CPU'larda LDAXR + STLXR veya LDADD kullanabilir: Konuları düz C ile nasıl başlatabilirim?

Ubuntu 19.10 AMD64, GCC 9.2.1, Lenovo ThinkPad P51'de test edilmiştir.

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.