理解C++11中的`memory_order_acquire`和`memory_order_release`

qlzsbp2j  于 2023-10-20  发布在  其他
关注(0)|答案(3)|浏览(156)

我正在阅读the documentation

memory_order_acquire:具有此内存顺序的加载操作在受影响的内存位置上执行获取操作:在此加载之前,当前线程中的任何读取或写入都不能被重新排序。其他线程中释放相同原子变量的所有写入在当前线程中可见(请参阅下面的释放-获取顺序)。
memory_order_release:具有此内存顺序的存储操作执行释放操作:在此存储之后,当前线程中的任何读取或写入都不能被重新排序。当前线程中的所有写入在获取相同原子变量的其他线程中可见(请参见下面的释放-获取排序),并且将依赖关系带入原子变量的写入在使用相同原子变量的其他线程中可见(请参见下面的释放-连接排序)

这两个比特:
memory_order_acquire
.在此加载之前,当前线程中的任何读取或写入都不能重新排序.
memory_order_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();
}

但我真的不知道我引用的两点在哪里适用。我明白发生了什么,但我真的没有看到重新排序位,因为代码很小。

kfgdxczn

kfgdxczn1#

一个线程所做的工作不保证对其他线程可见。
为了使数据在线程之间可见,需要一种同步机制。可以使用非松弛atomicmutex。这就是所谓的获取-释放语义。写互斥体会“释放”它之前的所有内存写操作,而阅读相同的互斥体会“获取”这些写操作。
在这里,我们使用ptr将到目前为止完成的工作(data = 42)“释放”到另一个线程:

data = 42;
    ptr.store(p, std::memory_order_release); // changes ptr from null to not-null

在这里,我们 * 等待 *,通过这样做,我们 * 同步 *(“获取”)生产者线程所做的工作:

while (!ptr.load(std::memory_order_acquire)) // assuming initially ptr is null
        ;
    assert(data == 42);

请注意两个不同的操作:
1.我们在线程之间等待(同步步骤)
1.作为 * 等待 * 的副作用,我们将工作从提供者 * 转移 * 到消费者(提供者 * 发布 *,消费者 * 获取 *)
在没有(2)的情况下,例如当使用memory_order_relaxed时,只有atomic值本身被同步。之前/之后完成的所有其他工作都不是,例如。data不一定包含42,并且在地址p处可能没有完全构造的string示例(如消费者所见)。
关于获取/释放语义的更多细节和C内存模型的其他细节,我建议观看Herb的优秀atomic<> weapons talk,它很长,但很有趣。关于更多细节,有一本书叫《C Concurrency in Action》。

7kqas0il

7kqas0il2#

获取和释放是记忆障碍。如果你的程序在一个获取障碍之后读取数据,你可以保证你将按照与任何其他线程关于同一个原子变量的任何先前释放一致的顺序阅读数据。原子变量在所有线程上的读写都保证有绝对顺序(当使用memory_order_acquirememory_order_release时,尽管提供了较弱的操作)。这些屏障实际上将该顺序传播到使用该原子变量的任何线程。你可以使用原子来表示某个东西已经“完成”或“准备好”了,但是如果消费者读取的内容超出了原子变量,消费者就不能依赖于“看到”其他内存的正确“版本”,原子的价值就有限了。
关于“moving before”或“moving after”的语句是对优化器的指示,它不应该重新排序操作以无序地发生。优化器非常擅长重新排序指令,甚至省略冗余的读/写,但如果它们跨越内存屏障重新组织代码,它们可能会无意中违反该顺序。
您的代码依赖于std::string对象(a)在ptr被赋值之前已经在producer()中构造,以及(b)该字符串的构造版本(即,它占用的存储器的版本)是consumer()读取的版本。简单地说,consumer()一看到ptr被赋值就会急切地读取字符串,所以它最好看到一个有效的、完全构造的对象,否则糟糕的时间将接踵而至。在这段代码中,赋值ptr的“行为”是producer()如何“告诉”consumer字符串“就绪”。记忆障碍的存在是为了确保消费者看到的是什么。
相反,如果ptr被声明为普通的std::string *,那么编译器可以决定优化p,并将分配的地址直接分配给ptr,然后才构造对象并分配int数据。对于consumer线程来说,这可能是一个灾难,它使用该赋值作为producer准备的对象已经就绪的指示器。准确地说,如果ptr是一个指针,consumer可能永远不会看到分配的值,或者在某些架构上读取部分分配的值,其中只有一些字节被分配,它指向垃圾内存位置。然而,这些方面是关于它是原子的,而不是更广泛的内存障碍。

r7s23pms

r7s23pms3#

如果使用std::memory_order_relaxed作为存储,编译器可以使用“as-if”规则将data = 42;移动到存储之后,并且consumer可以看到非空指针和不确定的data
如果使用std::memory_order_relaxed进行加载,编译器可以使用“as-if”规则将assert(data == 42);移动到加载循环之前。
这两个都是允许的,因为data的值与ptr的值无关
如果ptr是非原子的,那么就会有数据竞争,因此会有未定义的行为。

相关问题