std::atomic
많은 ISA가 직접 하드웨어를 지원하기 때문에 존재합니다
C ++ 표준에 대한 내용 std::atomic
은 다른 답변에서 분석되었습니다.
이제 std::atomic
다른 종류의 통찰력을 얻기 위해 컴파일되는 내용을 살펴 보겠습니다 .
이 실험에서 가장 중요한 점은 최신 CPU가 원자 정수 연산 (예 : x86의 LOCK 접두사)을 직접 지원하고 std::atomic
기본적으로 이러한 명령어에 대한 이식 가능한 인터페이스로 존재 한다는 것입니다. x86 어셈블리에서 "잠금"명령의 의미는 무엇입니까? aarch64에서는 LDADD 가 사용됩니다.
이 지원은 다음과 같은 일반적인 방법에 빠른 대안을 수 있습니다 std::mutex
보다 느린되는 비용으로, 더 복잡한 다중 명령 섹션 원자 만들 수 std::atomic
있기 때문에 std::mutex
이 만드는 futex
느린 방출 유저 랜드 지침보다 훨씬 리눅스에서 시스템 호출을 std::atomic
, 또한 참조 : 비 음주자 :: 울타리를 만들 뮤텍스 수 std?
사용되는 전 처리기 정의에 따라 서로 다른 동기화 메커니즘을 사용하여 여러 스레드에서 전역 변수를 증가시키는 다음 다중 스레드 프로그램을 고려해 봅시다.
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의 상류 .
컴파일, 실행 및 분해 :
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
메모리를 원자 적으로 가져 오기, 수정 및 업데이트합니다.
명시 적 인라인 어셈블리 LOCK 접두어 는 대신에를 사용 std::atomic
한다는 점을 제외하고는 거의 동일한 내용으로 컴파일됩니다 . INC가 1 바이트 더 작은 디코딩을 생성 한 것을 고려할 때 GCC가 왜을 선택했는지 잘 모르겠습니다 .inc
add
add
ARMv8은 최신 CPU에서 LDAXR + STLXR 또는 LDADD를 사용할 수 있습니다. 일반 C에서 스레드를 시작하려면 어떻게해야합니까?
Ubuntu 19.10 AMD64, GCC 9.2.1, Lenovo ThinkPad P51에서 테스트되었습니다.
a.fetch_add(12)
원자 RMW를 원할 경우 와 같은 것을 사용해야합니다 .