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
}