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 में परीक्षण किया गया।
a.fetch_add(12)
यदि आप एक परमाणु RMW चाहते हैं तो आपको कुछ का उपयोग करने की आवश्यकता है ।