क्या वास्तव में std :: परमाणु है?


174

मैं समझता हूं कि std::atomic<>यह एक परमाणु वस्तु है। लेकिन परमाणु किस हद तक? मेरी समझ में एक ऑपरेशन परमाणु हो सकता है। वास्तव में एक वस्तु को परमाणु बनाने से क्या मतलब है? उदाहरण के लिए यदि निम्नलिखित कोड को दो धागे समवर्ती रूप से निष्पादित करते हैं:

a = a + 12;

तब क्या पूरा ऑपरेशन (कहना add_twelve_to(int)) परमाणु है? या परिवर्तनशील परमाणु (इसलिए operator=()) में परिवर्तन किए गए हैं ?


9
a.fetch_add(12)यदि आप एक परमाणु RMW चाहते हैं तो आपको कुछ का उपयोग करने की आवश्यकता है ।
केरेके एसबी

हां, यही मैं नहीं समझता। किसी वस्तु को परमाणु बनाने से क्या तात्पर्य है। यदि कोई इंटरफ़ेस था तो इसे केवल म्यूटेक्स या मॉनिटर के साथ परमाणु बनाया जा सकता था।

2
@AaryamanSagar यह दक्षता के एक मुद्दे को हल करता है। म्यूटेक्स और मॉनिटर कम्प्यूटेशनल ओवरहेड ले जाते हैं। उपयोग std::atomicकरने से मानक पुस्तकालय तय करता है कि परमाणुता प्राप्त करने के लिए क्या आवश्यक है
ड्रू डॉर्मन

1
@ आर्यमानसागर: std::atomic<T>एक प्रकार है जो परमाणु संचालन के लिए अनुमति देता है । यह जादुई रूप से आपके जीवन को बेहतर नहीं बनाता है, फिर भी आपको यह जानना होगा कि आप इसके साथ क्या करना चाहते हैं। यह एक बहुत ही विशिष्ट उपयोग के मामले के लिए है, और परमाणु संचालन के उपयोग (वस्तु पर) आम तौर पर बहुत सूक्ष्म हैं और गैर-स्थानीय दृष्टिकोण से सोचा जाने की आवश्यकता है। इसलिए जब तक आप पहले से ही यह नहीं जानते हैं कि आप परमाणु संचालन क्यों चाहते हैं, तो यह प्रकार शायद आपके लिए बहुत काम का नहीं है।
केरेक एसबी

जवाबों:


188

प्रत्येक तात्कालिकता और एसटीडी का पूर्ण विशेषज्ञता :: परमाणु <> एक प्रकार का प्रतिनिधित्व करता है जो विभिन्न धागे एक साथ अपरिभाषित व्यवहार को बढ़ाए बिना (उनके उदाहरणों) पर काम कर सकते हैं:

परमाणु प्रकार की वस्तुएं केवल C ++ ऑब्जेक्ट हैं जो डेटा रेस से मुक्त हैं; यही है, अगर एक धागा परमाणु वस्तु को लिखता है जबकि दूसरा धागा उससे पढ़ता है, तो व्यवहार अच्छी तरह से परिभाषित है।

इसके अलावा, परमाणु वस्तुओं तक पहुंच अंतर-थ्रेड सिंक्रोनाइज़ेशन स्थापित कर सकती है और निर्दिष्ट गैर-परमाणु मेमोरी एक्सेस को ऑर्डर कर सकती है std::memory_order

std::atomic<>जीसीसी के मामले में MSVC या परमाणु bultins के साथ इंटरलॉक्ड फ़ंक्शन (उदाहरण के लिए) का उपयोग करके पूर्व-सी ++ 11 बार में, रैप्स ऑपरेशन किया गया था।

इसके अलावा, std::atomic<>आपको विभिन्न मेमोरी ऑर्डर की अनुमति देकर और अधिक नियंत्रण प्रदान करता है जो सिंक्रनाइज़ेशन और ऑर्डर की कमी को निर्दिष्ट करता है। यदि आप C ++ 11 एटमिक्स और मेमोरी मॉडल के बारे में अधिक पढ़ना चाहते हैं, तो ये लिंक उपयोगी हो सकते हैं:

ध्यान दें कि, विशिष्ट उपयोग के मामलों के लिए, आप शायद ओवरलोडेड अंकगणितीय संचालकों या उनमें से किसी अन्य सेट का उपयोग करेंगे :

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

चूँकि ऑपरेटर सिंटैक्स आपको मेमोरी ऑर्डर को निर्दिष्ट करने की अनुमति नहीं देता है, इन ऑपरेशनों के साथ प्रदर्शन किया जाएगा std::memory_order_seq_cst, क्योंकि यह C ++ 11 में सभी परमाणु संचालन के लिए डिफ़ॉल्ट आदेश है। यह सभी परमाणु संचालनों के बीच अनुक्रमिक संगतता (कुल वैश्विक आदेश) की गारंटी देता है।

कुछ मामलों में, हालांकि, इसकी आवश्यकता नहीं हो सकती है (और मुफ्त में कुछ भी नहीं आता है), इसलिए आप अधिक स्पष्ट रूप का उपयोग करना चाह सकते हैं:

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

अब, आपका उदाहरण:

a = a + 12;

एक एकल परमाणु सेशन का मूल्यांकन नहीं करेगा: इसका परिणाम a.load()(जो स्वयं परमाणु है) होगा, फिर इस मूल्य 12और a.store()(अंतिम परमाणु का भी) परिणाम होगा। जैसा कि मैंने पहले उल्लेख किया है, std::memory_order_seq_cstयहाँ उपयोग किया जाएगा।

हालांकि, यदि आप लिखते हैं a += 12, तो यह एक परमाणु ऑपरेशन होगा (जैसा कि मैंने पहले उल्लेख किया है) और लगभग बराबर है a.fetch_add(12, std::memory_order_seq_cst)

अपनी टिप्पणी के लिए:

एक नियमित रूप intसे परमाणु भार और भंडार होते हैं। इसके साथ लपेटने की बात क्या है atomic<>?

आपका कथन केवल उन आर्किटेक्चर के लिए सही है जो स्टोर और / या लोड के लिए परमाणुता की ऐसी गारंटी प्रदान करते हैं। ऐसे आर्किटेक्चर हैं जो ऐसा नहीं करते हैं। इसके अलावा, आमतौर पर यह आवश्यक होता है कि ऑपरेशन शब्द पर किया जाना चाहिए- / dword- संरेखित पता परमाणु होना std::atomic<>एक ऐसी चीज है जो बिना किसी अतिरिक्त आवश्यकता के हर मंच पर परमाणु होने की गारंटी है । इसके अलावा, यह आपको इस तरह कोड लिखने की अनुमति देता है:

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);
}

ध्यान दें कि दावे की स्थिति हमेशा सच होगी (और इस प्रकार, कभी भी ट्रिगर नहीं होगी), इसलिए आप हमेशा यह सुनिश्चित कर सकते हैं कि whileलूप बिट्स के बाद डेटा तैयार है। वह है क्योंकि:

  • store()झंडा sharedDataसेट होने के बाद किया जाता है (हम मानते हैं कि generateData()हमेशा कुछ उपयोगी देता है, विशेष रूप से, कभी नहीं लौटता है NULL) और std::memory_order_releaseआदेश का उपयोग करता है :

memory_order_release

इस मेमोरी ऑर्डर के साथ एक स्टोर ऑपरेशन रिलीज़ ऑपरेशन करता है : इस स्टोर के बाद वर्तमान थ्रेड में कोई भी लिखा या लिखा नहीं जा सकता है । वर्तमान धागे में सभी अन्य धागे में दिखाई देते हैं जो समान परमाणु चर प्राप्त करते हैं

  • sharedDatawhileलूप से बाहर निकलने के बाद उपयोग किया जाता है, और इस तरह load()ध्वज के बाद एक गैर-शून्य मान लौटाएगा। आदेश load()का उपयोग करता है std::memory_order_acquire:

std::memory_order_acquire

इस मेमोरी ऑर्डर के साथ एक लोड ऑपरेशन प्रभावित मेमोरी लोकेशन पर अधिग्रहण ऑपरेशन करता है : इस लोड से पहले कोई भी रीड या राइट नहीं लिखा जा सकता है । सभी अन्य थ्रेड्स में लिखते हैं जो उसी परमाणु चर को जारी करते हैं जो वर्तमान थ्रेड में दिखाई देते हैं

यह आपको सिंक्रोनाइज़ेशन पर सटीक नियंत्रण देता है और आपको यह स्पष्ट रूप से निर्दिष्ट करने की अनुमति देता है कि आपका कोड कैसे व्यवहार कर सकता है / नहीं / नहीं कर सकता है / नहीं। यह संभव नहीं होगा यदि केवल गारंटी ही परमाणुता थी। खासकर जब यह रिलीज-उपभोग आदेश जैसे बहुत दिलचस्प सिंक मॉडल की बात आती है ।


2
क्या वास्तव में ऐसे आर्किटेक्चर हैं जिनके पास आदिमों के लिए परमाणु भार और भंडार नहीं हैं int?

7
यह केवल परमाणुवाद के बारे में नहीं है। यह ऑर्डर करने, मल्टी-कोर सिस्टम में व्यवहार आदि के बारे में भी है। आप इस लेख को पढ़ना चाहते हैं ।
माटूस ग्रेजजेक

4
@AaryamanSagar अगर मैं गलत नहीं हूँ, यहाँ तक कि x86 पर भी लिखता है और लिखता है केवल शब्द सीमाओं पर गठबंधन होने पर परमाणु।
v.shshenko 11

@MateuszGrzejek मैंने एक परमाणु प्रकार का संदर्भ लिया है। यदि आप निम्नलिखित को अभी भी सत्यापित कर सकते हैं कि क्या वस्तु असाइनमेंट पर परमाणु संचालन की गारंटी होगी ideone.com/HpSwqo
xAditya3393

3
@TimMB हां, आम तौर पर, आपके पास (कम से कम) दो स्थितियां होंगी, जहां निष्पादन के क्रम में बदलाव किया जा सकता है: (1) कंपाइलर निर्देशों को फिर से व्यवस्थित कर सकता है (जितना मानक अनुमति देता है) आउटपुट कोड का बेहतर प्रदर्शन प्रदान करने के लिए (सीपीयू रजिस्टरों, भविष्यवाणियों आदि के उपयोग के आधार पर) और (2) सीपीयू एक अलग क्रम में निर्देशों को निष्पादित कर सकते हैं, उदाहरण के लिए, कैश सिंक पॉइंट्स की संख्या को कम करें। के लिए प्रदान की गई बाधाओं का आदेश std::atomic( std::memory_order) उन सीमाओं को सीमित करने के उद्देश्य से कार्य करता है जिन्हें होने की अनुमति है।
मेटूस ग्रेजेक

20

मैं समझता हूं कि std::atomic<>एक वस्तु परमाणु बनाती है।

यह एक परिप्रेक्ष्य की बात है ... आप इसे मनमानी वस्तुओं पर लागू नहीं कर सकते हैं और उनके संचालन परमाणु हो सकते हैं, लेकिन (अधिकांश) अभिन्न प्रकारों और संकेत के लिए प्रदान की गई विशेषज्ञता का उपयोग किया जा सकता है।

a = a + 12;

std::atomic<>करता है (टेम्पलेट अभिव्यक्ति का उपयोग करें) एक एकल परमाणु ऑपरेशन के लिए इसे सरल operator T() const volatile noexceptकरता है , इसके बजाय सदस्य एक परमाणु करता load()है a, फिर बारह को जोड़ा जाता है, और operator=(T t) noexceptएक करता है store(t)


यही मैं पूछना चाहता था। एक नियमित इंट में परमाणु भार और भंडार होते हैं। इसे परमाणु <>

8
@AaryamanSagar बस एक सामान्य intको संशोधित करने से यह सुनिश्चित नहीं होता है कि परिवर्तन अन्य थ्रेड्स से दिखाई दे रहा है, और न ही इसे पढ़ने से यह सुनिश्चित होता है कि आप अन्य थ्रेड्स में बदलाव देखते हैं, और कुछ चीजें जैसे my_int += 3कि जब तक आप उपयोग नहीं करते हैं तब तक गारंटी नहीं दी जाती है std::atomic<>- वे इसमें शामिल हो सकते हैं एक फ़ेच, फिर क्रम जोड़ें, फिर स्टोर करें, जिसमें समान मान को अपडेट करने का प्रयास करने वाले कुछ अन्य थ्रेड भ्रूण के बाद और स्टोर से पहले आ सकते हैं, और अपने थ्रेड के अपडेट को क्लोब कर सकते हैं।
टोनी डेलरो

" बस एक सामान्य इंट को संशोधित करने से यह सुनिश्चित नहीं होता है कि परिवर्तन अन्य थ्रेड्स से दिखाई दे रहा है " यह इससे भी बदतर है: उस दृश्यता को मापने का कोई भी प्रयास यूबी में परिणाम होगा।
जिज्ञासु

8

std::atomic मौजूद है क्योंकि कई ISAs को इसके लिए प्रत्यक्ष हार्डवेयर समर्थन है

सी ++ मानक के बारे std::atomicमें क्या कहता है इसका विश्लेषण अन्य उत्तरों में किया गया है।

तो अब देखते हैं कि std::atomicएक अलग तरह की अंतर्दृष्टि प्राप्त करने के लिए क्या संकलन है।

इस प्रयोग से मुख्य संकेत यह है कि आधुनिक सीपीयू का परमाणु पूर्णांक संचालन के लिए प्रत्यक्ष समर्थन है, उदाहरण के लिए x86 में LOCK उपसर्ग, और std::atomicमूल रूप से उन घुसपैठों के लिए एक पोर्टेबल इंटरफ़ेस के रूप में मौजूद है: x86 विधानसभा में "लॉक" निर्देश का क्या मतलब है? Anarch64 में, LDADD का उपयोग किया जाएगा।

यह समर्थन इस तरह के रूप में अधिक सामान्य तरीकों को तेजी से विकल्पों की अनुमति देता है std::mutexकी तुलना में धीमी जा रहा है की कीमत पर, है, जो और अधिक जटिल बहु शिक्षा वर्गों परमाणु कर सकते हैं std::atomicक्योंकि std::mutexयह बनाता है futexलिनक्स में सिस्टम कॉल, जो द्वारा उत्सर्जित userland निर्देश की तुलना में धीमी रास्ता है std::atomic, इसे भी देखें: क्या std :: mutex एक बाड़ बनाता है?

आइए निम्नलिखित बहु-थ्रेडेड प्रोग्राम पर विचार करें जो विभिन्न थ्रेड्स में एक वैश्विक चर को बढ़ाता है, जिसमें विभिन्न तुल्यकालन तंत्र होते हैं, जिसके आधार पर प्रीप्रोसेसर परिभाषित किया जाता है।

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;
}

गिटहब ऊपर

संकलित करें, चलाएं और जुदा करें:

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

अत्यधिक "गलत" दौड़ हालत के लिए उत्पादन की संभावना main_fail.out:

expect 400000
global 100000

और दूसरों के निर्धारक "सही" आउटपुट:

expect 400000
global 400000

का विघटन 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

का विघटन 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   

का विघटन 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

निष्कर्ष:

  • गैर-परमाणु संस्करण वैश्विक को एक रजिस्टर में बचाता है, और रजिस्टर में वृद्धि करता है।

    इसलिए, अंत में, बहुत संभव है कि चार लिखते हैं वैश्विक के साथ वापस उसी "गलत" मूल्य के साथ होता है 100000

  • std::atomicके लिए संकलित करता है lock addq। LOCK उपसर्ग निम्नलिखित incभ्रूण को, संशोधित करता है और स्मृति को परमाणु रूप से अद्यतन करता है।

  • हमारे स्पष्ट इनलाइन असेंबली लॉक प्रीफिक्स उपसर्ग को लगभग उसी चीज़ के लिए संकलित करता है std::atomic, सिवाय इसके कि हमारे incबजाय इसका उपयोग किया जाता है add। यह निश्चित नहीं है कि जीसीसी ने क्यों चुना add, यह देखते हुए कि हमारे इंक ने एक डिकोडिंग 1 बाइट को छोटा किया।

ARMv8 या तो नए CPU में LDAXR + STLXR या LDADD का उपयोग कर सकता है: मैं सादे सी में थ्रेड कैसे शुरू करूं?

Ubuntu 19.10 AMD64, GCC 9.2.1, लेनोवो थिंकपैड P51 में परीक्षण किया गया।

हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.