C++ std::内存顺序和指令顺序,澄清
这是我们的后续问题 我想准确地理解指令顺序的含义,以及它是如何受到C++ std::内存顺序和指令顺序,澄清,c++,c++11,atomic,memory-model,stdatomic,C++,C++11,Atomic,Memory Model,Stdatomic,这是我们的后续问题 我想准确地理解指令顺序的含义,以及它是如何受到std::memory\u order\u acquire,std::memory\u order\u release等的影响的 在我链接的问题中,已经提供了一些细节,但我觉得提供的答案并不是关于订单(这更符合我的要求),而是激发了一点动机,说明为什么这是必要的等等 我将引用我将用作参考的相同示例 #include <thread> #include <atomic> #include <casser
std::memory\u order\u acquire
,std::memory\u order\u release
等的影响的
在我链接的问题中,已经提供了一些细节,但我觉得提供的答案并不是关于订单(这更符合我的要求),而是激发了一点动机,说明为什么这是必要的等等
我将引用我将用作参考的相同示例
#include <thread>
#include <atomic>
#include <cassert>
#include <string>
std::atomic<std::string*> ptr;
int data;
void producer()
{
std::string* p = new std::string("Hello");
data = 42;
ptr.store(p, std::memory_order_release);
}
void consumer()
{
std::string* p2;
while (!(p2 = ptr.load(std::memory_order_acquire)))
;
assert(*p2 == "Hello"); // never fires
assert(data == 42); // never fires
}
int main()
{
std::thread t1(producer);
std::thread t2(consumer);
t1.join(); t2.join();
}
及
根据文件,重点放在第一个方面
。。。在此存储之后,当前线程中的任何读取或写入都无法重新排序
我一直在看一些谈话来理解这个订购问题,我现在明白为什么它很重要了。我还不能完全理解编译器是如何翻译顺序规范的,我认为文档中给出的示例也不是特别有用,因为在运行producer
的线程中执行存储操作后,没有其他指令,因此无论如何都不会重新排序。然而也有可能我误解了,有没有可能他们的意思是,相当于
std::string* p = new std::string("Hello");
data = 42;
ptr.store(p, std::memory_order_release);
会不会这样翻译的前两行永远不会移动到原子存储之后?
同样,在线程运行生成器中,是否有可能在原子加载之前不会移动任何断言(或等效程序集)?假设我在存储之后有第三条指令,那么那些已经在原子加载之后的指令会怎么样呢
我还尝试编译这样的代码,用-S
标志保存中间汇编代码,但它非常大,我真的无法理解
再次澄清,这个问题是关于排序的,而不是关于为什么这些机制有用或必要。没有原子:
std::string* ptr;
int data;
void producer()
{
std::string* p = new std::string("Hello");
data = 42;
ptr = p;
}
void consumer()
{
std::string* p2;
while (!(p2 = ptr))
;
assert(*p2 == "Hello"); // never fires
assert(data == 42); // never fires
}
在producer
中,编译器可以在赋值到ptr之后将赋值自由移动到数据。因为ptr在设置数据之前变为非空,所以可以触发相应的断言
发行版存储区禁止编译器这样做
在消费者
中,编译器可以自由地将数据上的断言移动到循环之前
load acquire禁止编译器执行此操作
与排序无关,但编译器可以完全忽略循环,因为如果循环开始时ptr为null,则没有任何东西可以有效地使其显示为非null,从而导致无限循环,也可以假定该循环不会发生
我认为文档中给出的示例也不特别重要
也很有用,因为在线程运行的存储操作之后
制片人:没有其他指示,因此不会有任何指示
无论如何,我们都要重新订购
如果有,他们可以提前执行。那会怎么样
生产者必须保证的唯一一件事是,在设置标志之前,内存中的“产品”已完全写入;否则,消费者将无法避免读取未初始化的内存(或对象的旧值)
设置发布对象太晚将是灾难性的。但是,如何开始设置另一个已发布对象(例如第二个对象)“太早”是一个问题
你怎么会过早知道制作人在做什么?唯一允许您做的事情是检查标志,并且只有设置标志后,您才能观察发布的对象
因此,如果在修改标志之前对任何内容进行了重新排序,您应该无法看到它
但在x86-64上的应用程序中看不到任何内容:
producer():
sub rsp, 8
mov edi, 32
call operator new(unsigned long)
mov DWORD PTR data[rip], 42
lea rdx, [rax+16]
mov DWORD PTR [rax+16], 1819043144
mov QWORD PTR [rax], rdx
mov BYTE PTR [rax+20], 111
mov QWORD PTR [rax+8], 5
mov BYTE PTR [rax+21], 0
mov QWORD PTR ptr[abi:cxx11][rip], rax
add rsp, 8
ret
(如果您想知道,ptr[abi:cx11]
是一个修饰名,而不是一些时髦的asm语法,因此ptr[abi:cx11][rip]
的意思是ptr[rip]
)
可概括为:
setup stack frame
assign data
setup string object
assign ptr
remove frame and return
设置堆栈帧
分配数据
设置字符串对象
分配ptr
拆下框架并返回
所以实际上没有什么值得注意的,除了ptr
是最后分配的
您必须选择另一个目标才能看到更有趣的内容。回答您的评论可能会有用:
我仍然觉得我的问题不清楚,我的问题更像是这样 跟随。假设(例如在producer中)您再添加几个 原子存储之后的语句,例如data_2=175和 a data_3=10,其中data_2和data_3是全局变量。具体情况如何 现在重新订购受影响吗?我知道你可能把这件事写在 你的回答,所以如果我很烦人,我向你道歉 让我们来摆弄一下你的
producer()
可以consumer()
在“数据”中找到值41。否。42的值已(逻辑上)存储到释放围栏点的数据中,如果consumer()
找到42的值,则42的存储(至少看起来)将发生在释放围栏之后
好的,现在让我们进一步修改
void producer()
{
data = 0xFF01;
std::string* p = new std::string("Hello");
data = 0xFF02;
ptr.store(p, std::memory_order_release);
data = 0x0003
}
现在所有的赌注都输光了<代码>数据不是原子的,也不能保证消费者会找到什么。在大多数体系结构上,现实情况是,唯一的候选对象是0xFF02或0x0003,但肯定有一些体系结构可能会找到0xFF03和/或0x0002。如果一个16位int
被写为2个单字节操作(从任意一个“端”开始),那么在一个具有8位总线的体系结构上可能会发生这种情况
但原则上,面对这样的数据竞争,现在根本无法保证存储什么。这是一场数据竞赛,因为没有控制来确保
消费者
是否通过额外的写入进行排序。我知道,在内存排序方面,人们通常试图争论是否以及如何对操作进行重新排序,但我认为这是错误的方法!C++标准没有说明指令是如何重新排序的,而是定义了发生在关系之前的,它本身就是B。
setup stack frame
assign data
setup string object
assign ptr
remove frame and return
void producer()
{
data = 41;
std::string* p = new std::string("Hello");
data = 42;
ptr.store(p, std::memory_order_release);
}
void producer()
{
data = 0xFF01;
std::string* p = new std::string("Hello");
data = 0xFF02;
ptr.store(p, std::memory_order_release);
data = 0x0003
}