我知道std::atomic<>是一个原子对象。但是原子到什么程度呢?根据我的理解,操作可以是原子的。使一个对象原子化到底意味着什么?例如,如果有两个线程并发执行以下代码:
std::atomic<>
a = a + 12;
那么整个操作(比如add_twelve_to(int))是原子的吗?还是对变量atomic(所以operator=())进行了更改?
add_twelve_to(int)
operator=()
我知道std::atomic<>使对象具有原子性。
这是一个角度的问题……您不能将其应用于任意对象并使其操作成为原子的,但可以使用为(大多数)整型和指针提供的专门化。
std::atomic<>没有(使用模板表达式)将其简化为单个原子操作,相反,operator T() const volatile noexcept成员执行a的原子load(),然后添加12,operator=(T t) noexcept执行store(t)。
operator T() const volatile noexcept
a
load()
operator=(T t) noexcept
store(t)
std:: atomic< >的每个实例化和完全特化表示一种类型,不同的线程可以同时操作(它们的实例),而不会引发未定义的行为:
原子类型的对象是唯一没有数据争用的c++对象;也就是说,如果一个线程写入一个原子对象,而另一个线程从中读取,则行为是定义良好的。 此外,对原子对象的访问可以建立线程间同步,并按std::memory_order指定的顺序对非原子内存访问进行排序。
原子类型的对象是唯一没有数据争用的c++对象;也就是说,如果一个线程写入一个原子对象,而另一个线程从中读取,则行为是定义良好的。
此外,对原子对象的访问可以建立线程间同步,并按std::memory_order指定的顺序对非原子内存访问进行排序。
std::memory_order
std::atomic<>包装的操作,在c++之前的11次中,必须使用(例如)MSVC的联锁功能或GCC的原子bultins来执行。
此外,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::memory_order_seq_cst
然而,在某些情况下,这可能不是必需的(没有什么是免费的),所以你可能想使用更显式的形式:
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
现在,你的例子:
不会计算为单个原子op:它将导致a.load()(它本身是原子的),然后将该值与最终结果的12和a.store()(也是原子的)相加。如前所述,这里将使用std::memory_order_seq_cst。
a.load()
12
a.store()
然而,如果你写a += 12,它将是一个原子操作(正如我前面提到的),大致相当于a.fetch_add(12, std::memory_order_seq_cst)。
a += 12
a.fetch_add(12, std::memory_order_seq_cst)
至于你的评论:
常规int具有原子加载和存储。用atomic<>包装它的意义是什么?
int
atomic<>
你的说法只适用于为存储和/或负载提供原子性保证的架构。有些体系结构不这样做。此外,通常要求必须在字/字对齐的地址上执行操作才能是原子的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循环退出后数据已经准备好。这是因为:
while
store()
sharedData
generateData()
NULL
std::memory_order_release
memory_order_release 具有此内存顺序的存储操作执行释放 操作:当前线程的所有读写操作都不能被重新排序 后这家商店。当前线程的所有写操作在 获得相同原子变量的其他线程
memory_order_release
具有此内存顺序的存储操作执行释放 操作:当前线程的所有读写操作都不能被重新排序 后这家商店。当前线程的所有写操作在 获得相同原子变量
std::memory_order_acquire
std::memory_order_acquire 具有此内存顺序的加载操作执行收购操作 在受影响的内存位置上:当前没有读写 线程可以重新排序之前此加载。所有在其他线程中的写入 释放相同的原子变量在当前可见 线程< /强>。< / p >
这使您可以精确地控制同步,并允许您显式地指定您的代码可能/可能不/将/将不表现。如果仅仅保证原子性本身,这是不可能的。特别是当涉及到非常有趣的同步模型,如release-consume订购。
std::atomic的存在是因为许多isa对它有直接的硬件支持
std::atomic
c++标准关于std::atomic的解释已经在其他答案中进行了分析。
所以现在让我们看看std::atomic编译为什么,以获得不同的见解。
这个实验的主要结论是,现代cpu直接支持原子整数操作,例如x86中的LOCK前缀,而std::atomic基本上是作为这些指令的可移植接口而存在的:
这种支持允许更快地替代更通用的方法,如std::mutex,它可以使更复杂的多指令部分原子化,代价是比std::atomic慢,因为std::mutex在Linux中进行futex系统调用,这比std::atomic发出的用户域指令慢得多,另参见:std::互斥创建一个围栏吗?
std::mutex
futex
让我们考虑下面的多线程程序,它在多个线程之间递增一个全局变量,根据使用的预处理器定义使用不同的同步机制。
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的竞态条件输出:
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的分解:
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的分解:
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
结论:
非原子版本将全局变量保存到寄存器中,并增加寄存器。
因此,在最后,很可能有4个写入以相同的“错误”返回全局。值100000。
100000
std::atomic编译为lock addq。LOCK前缀使下面的inc原子地获取、修改和更新内存。
lock addq
inc
我们的显式内联程序集LOCK前缀编译为几乎与std::atomic相同的东西,除了我们的inc被使用而不是add。不确定为什么GCC选择add,考虑到我们的INC生成了一个小1字节的解码。
add
ARMv8可以在较新的cpu: 如何在普通C中启动线程?中使用LDAXR + STLXR或LDADD
在Ubuntu 19.10 AMD64, GCC 9.2.1,联想ThinkPad P51上测试。