我知道std::atomic<>
是一个原子对象。但原子到什么程度?根据我的理解,操作可以是原子的。使对象成为原子意味着什么?例如,如果有两个线程同时执行以下代码:
a = a + 12;
然后是整个操作(比如说add_twelve_to(int)
)原子?或者是变量原子的变化(所以operator=()
)?
答案 0 :(得分:109)
std::atomic<>的每个实例化和完全特化都代表一种类型,不同的线程可以同时对它们的实例进行操作,而不会引发未定义的行为:
原子类型的对象是唯一没有数据竞争的C ++对象;也就是说,如果一个线程写入原子对象而另一个线程从中读取,则行为是明确定义的。
此外,对原子对象的访问可以建立线程间同步并按
std::memory_order
指定的顺序进行非原子内存访问。
std::atomic<>
包装操作,在C ++之前的11次中,必须使用(例如)interlocked functions与MSVC或atomic bultins在GCC的情况下执行。
此外,std::atomic<>
通过允许指定同步和排序约束的各种memory orders为您提供更多控制。如果您想了解有关C ++ 11原子和内存模型的更多信息,这些链接可能很有用:
请注意,对于典型用例,您可能会使用overloaded arithmetic operators或another set of them:
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<>
包裹它?
您的陈述仅适用于为商店和/或负载提供原子性保证的体系结构。有些架构不会这样做。此外,通常需要的是,必须在字/双字对齐的地址上执行操作才能成为原子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
)并使用{ {1}}订单:
std::memory_order_release
具有此内存订单的商店操作会执行发布 操作:当前线程中没有读取或写入可以重新排序 此商店后 当前线程中的所有写入都可见 获取相同原子变量的其他线程
memory_order_release
循环退出后使用sharedData
,因此在flag while
之后将返回非零值。 load()
使用load()
订单:
std::memory_order_acquire
具有此内存顺序的加载操作会执行获取操作 在受影响的内存位置:当前没有读取或写入 线程可以重新排序之前此加载。 所有写入其他线程 释放相同的原子变量在当前可见 螺纹强>
这使您可以精确控制同步,并允许您明确指定代码可能/可能不会/将要/不会表现的方式。如果只保证是原子性本身,这是不可能的。特别是涉及非常有趣的同步模型,如release-consume ordering。
答案 1 :(得分:15)
我理解
std::atomic<>
使对象成为原子。
这是一个透视问题......你不能将它应用于任意对象并使它们的操作变为原子,但是可以使用为(大多数)整数类型和指针提供的特化。
a = a + 12;
std::atomic<>
没有(使用模板表达式)将此简化为单个原子操作,而operator T() const volatile noexcept
成员执行load()
的原子a
,然后十二是已添加,operator=(T t) noexcept
执行了store(t)
。
答案 2 :(得分:-2)
我们来分解一些东西
C ++标准关于std::atomic
的说法在其他答案中得到了分析。
因此,现在让我们来看看std::atomic
所编译得到的东西,以获得另一种见解。
此实验的主要成果是现代CPU直接支持原子整数操作,例如x86中的LOCK前缀,并且std::atomic
主要作为这些指令的可移植接口存在:What does the "lock" instruction mean in x86 assembly?在aarch64中,将使用LDADD。
此支持允许更快地替代更通用的方法,例如std::mutex
,这可以使更复杂的多指令节成为原子,其代价是比std::atomic
慢,从而使futex系统调用在Linux中,另请参见:Does std::mutex create a fence?
让我们考虑下面的多线程程序,该程序在多个线程之间增加全局变量,并根据所使用的预处理器定义使用不同的同步机制。
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
自动获取,修改和更新内存。
我们的显式内联汇编LOCK前缀编译为与std::atomic
几乎相同,除了使用我们的inc
代替add
。考虑到我们的INC产生的解码小1字节,因此不确定为什么GCC选择add
。
ARMv8可以在以下CPU中使用LDAXR + STLXR或LDADD:How do I start threads in plain C?
联想ThinkPad P51在Ubuntu 19.10 AMD64中进行了测试。