129 c++ multithreading atomic c++11
我明白这std::atomic<>是一个原子对象.但原子到什么程度?根据我的理解,操作可以是原子的.使对象成为原子意味着什么?例如,如果有两个线程同时执行以下代码:
a = a + 12;
Run Code Online (Sandbox Code Playgroud)
然后是整个操作(比方说add_twelve_to(int))原子?或者是变量原子(so operator=())的变化?
Mat*_*jek 140
std :: atomic <>的每个实例化和完全特化都表示一种类型,不同的线程可以同时对它们的实例进行操作,而不会引发未定义的行为:
原子类型的对象是唯一没有数据竞争的C++对象; 也就是说,如果一个线程写入原子对象而另一个线程从中读取,则行为是明确定义的.
此外,对原子对象的访问可以建立线程间同步并按照指定的顺序排序非原子内存访问
std::memory_order.
std::atomic<>包装操作,在C++之前的11次,必须使用(例如)GCC情况下的MSVC或原子文本的互锁函数来执行.
此外,std::atomic<>通过允许指定同步和排序约束的各种内存顺序,为您提供更多控制.如果您想了解有关C++ 11原子和内存模型的更多信息,这些链接可能很有用:
请注意,对于典型用例,您可能会使用重载算术运算符或其他一组:
std::atomic<long> value(0);
value++; //This is an atomic op
value += 5; //And so is this
Run Code Online (Sandbox Code Playgroud)
因为运算符语法不允许您指定内存顺序,所以这些操作将使用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
Run Code Online (Sandbox Code Playgroud)
现在,你的例子:
a = a + 12;
Run Code Online (Sandbox Code Playgroud)
不会评估单个原子操作:它将导致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);
}
Run Code Online (Sandbox Code Playgroud)
注意,断言条件将始终为真(因此,永远不会触发),因此您始终可以确定,在while循环退出后数据已准备就绪.那是因为:
store()sharedData设置后执行到标志(我们假设,generateData()总是返回有用的东西,特别是永远不会返回NULL)并使用std::memory_order_release顺序:
memory_order_release具有此内存顺序的存储操作将执行释放 操作:在此存储之后,不能对当前线程中的读取或写入进行重新排序 .当前线程中的所有写入在获取相同原子变量的其他线程中是可见的
sharedData在while循环退出后使用,因此在load()from flag之后将返回非零值.load()使用std::memory_order_acquire顺序:
std::memory_order_acquire具有此内存顺序的加载操作会对受影响的内存位置执行获取操作:在此加载之前,不能对当前线程中的读取或写入进行重新排序.在当前线程中可以看到释放相同原子变量的其他线程中的所有写入.
这使您可以精确控制同步,并允许您明确指定代码可能/可能不会/将要/不会表现的方式.如果只保证是原子性本身,这是不可能的.特别是当涉及非常有趣的同步模型,如发布消耗排序.
Cir*_*四事件 20
std::atomic 存在是因为许多 ISA 有直接的硬件支持
C++ 标准所说的内容std::atomic已在其他答案中进行了分析。
所以现在让我们看看std::atomic编译成什么以获得不同的洞察力。
这个实验的主要内容是现代 CPU 直接支持原子整数操作,例如 x86 中的 LOCK 前缀,并且std::atomic基本上作为这些指令的可移植接口存在:x86 汇编中的“锁定”指令是什么意思?在 aarch64 中,将使用LDADD。
这种支持能够实现更快的替代更一般的方法,例如std::mutex,它可以使更多的复杂的多指令部的原子,在比慢的成本std::atomic,因为std::mutex它使得futex在Linux系统调用,这是远远比发出的用户态指令较慢std::atomic,另请参阅:std::mutex 是否创建围栏?
让我们考虑以下多线程程序,它跨多个线程递增全局变量,根据使用的预处理器定义具有不同的同步机制。
主程序
#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;
}
Run Code Online (Sandbox Code Playgroud)
编译、运行和反汇编:
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
Run Code Online (Sandbox Code Playgroud)
极有可能的“错误”竞争条件输出main_fail.out:
expect 400000
global 100000
Run Code Online (Sandbox Code Playgroud)
和其他人的确定性“正确”输出:
expect 400000
global 400000
Run Code Online (Sandbox Code Playgroud)
拆卸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
Run Code Online (Sandbox Code Playgroud)
拆卸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
Run Code Online (Sandbox Code Playgroud)
拆卸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
Run Code Online (Sandbox Code Playgroud)
结论:
非原子版本将全局保存到寄存器,并递增寄存器。
因此,最后,很可能四次写入返回到全局,并且具有相同的“错误”值100000。
std::atomic编译为lock addq. LOCK 前缀使以下操作inc以原子方式获取、修改和更新内存。
我们的显式内联程序集 LOCK 前缀编译为与 几乎相同的东西std::atomic,除了使用 ourinc代替add. 不知道为什么 GCC 选择add,考虑到我们的 INC 生成的解码小 1 个字节。
ARMv8 可以在较新的 CPU 中使用 LDAXR + STLXR 或 LDADD:如何在普通 C 中启动线程?
在 Ubuntu 19.10 AMD64、GCC 9.2.1、联想 ThinkPad P51 中测试。
Ton*_*roy 17
我明白这
std::atomic<>使得一个对象成为原子.
这是一个透视问题......你不能将它应用于任意对象并使它们的操作变为原子,但可以使用为(大多数)整数类型和指针提供的特化.
a = a + 12;
std::atomic<>不(使用模板表达式)简化这对单个原子操作,而不是operator T() const volatile noexcept构件确实的原子load()的a,那么12被添加,并operator=(T t) noexcept做了store(t).
| 归档时间: |
|
| 查看次数: |
90949 次 |
| 最近记录: |