std::atomic
tồn tại bởi vì nhiều ISA có hỗ trợ phần cứng trực tiếp cho nó
Những gì tiêu chuẩn C ++ nói về std::atomic
đã được phân tích trong các câu trả lời khác.
Vì vậy, bây giờ hãy xem những gì std::atomic
biên dịch để có được một loại hiểu biết khác nhau.
Điểm nổi bật chính của thí nghiệm này là các CPU hiện đại có hỗ trợ trực tiếp cho các hoạt động số nguyên tử, ví dụ tiền tố LOCK trong x86 và std::atomic
về cơ bản tồn tại dưới dạng giao diện di động cho các nội dung đó: Lệnh "khóa" có nghĩa gì trong lắp ráp x86? Trong aarch64, LDADD sẽ được sử dụng.
Sự hỗ trợ này cho phép thay thế nhanh hơn cho các phương thức tổng quát hơn, chẳng hạn như std::mutex
có thể tạo ra các phần đa hướng dẫn phức tạp hơn, với chi phí chậm hơn std::atomic
vì std::mutex
nó thực hiện futex
các cuộc gọi hệ thống trong Linux, chậm hơn so với các hướng dẫn sử dụng được phát ra từ std::atomic
, xem thêm: Std :: mutex có tạo ra hàng rào không?
Chúng ta hãy xem xét chương trình đa luồng sau đây làm tăng biến toàn cục trên nhiều luồng, với các cơ chế đồng bộ hóa khác nhau tùy thuộc vào định nghĩa tiền xử lý nào được sử dụng.
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 ngược dòng .
Biên dịch, chạy và tháo rời:
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
Rất có khả năng đầu ra điều kiện cuộc đua "sai" cho main_fail.out
:
expect 400000
global 100000
và đầu ra "đúng" xác định của những người khác:
expect 400000
global 400000
Tháo gỡ 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
Tháo gỡ 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
Tháo gỡ 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
Kết luận:
phiên bản phi nguyên tử lưu toàn cầu vào một thanh ghi và tăng thanh ghi.
Do đó, cuối cùng, rất có thể bốn bài viết trở lại toàn cầu với cùng giá trị "sai" 100000
.
std::atomic
biên dịch thành lock addq
. Tiền tố LOCK thực hiện quá trình inc
tìm nạp, sửa đổi và cập nhật bộ nhớ sau đây .
tiền tố LOCK lắp ráp nội tuyến rõ ràng của chúng tôi biên dịch thành gần giống như std::atomic
, ngoại trừ việc chúng tôi inc
được sử dụng thay vì add
. Không chắc chắn tại sao GCC chọn add
, xem xét rằng INC của chúng tôi tạo ra giải mã nhỏ hơn 1 byte.
ARMv8 có thể sử dụng LDAXR + STLXR hoặc LDADD trong các CPU mới hơn: Làm cách nào để bắt đầu các luồng trong C đơn giản?
Đã thử nghiệm trong Ubuntu 19.10 AMD64, GCC 9.2.1, Lenovo ThinkPad P51.
a.fetch_add(12)
nếu bạn muốn có một RMW nguyên tử.