C11/C++11 内存模型获取、释放、宽松的细节

C11/C++11 memory model acquire, release, relaxed specifics

我对 C++11/C11 内存模型有一些疑问,想知道是否有人可以澄清。这些是关于 model/abstract 机器的问题,而不是关于任何真实架构的问题。


  1. acquire/release 效果是否保证从一个线程“级联”到下一个线程?

这是我的意思的伪代码示例(假设所有变量都从 0 开始)

[Thread 1]
store_relaxed(x, 1);
store_release(a, 1);

[Thread 2]
while (load_acquire(a) == 0);
store_release(b, 1);

[Thread 3]
while (load_acquire(b) == 0);
assert(load_relaxed(x) == 1);

线程 3 的获取与线程 2 的释放同步,线程 2 的获取与线程 1 的释放同步。因此,线程 3 一定会看到线​​程 1 设置为 x 的值,对吗?或者我们是否需要在这里使用 seq cst 以确保断言不会触发?我觉得 acquire/release 就足够了,但我找不到任何简单的解释来保证它。 acquire/release 的大部分解释主要集中在获取线程接收释放线程所做的所有存储。然而,在上面的示例中,线程 2 从不接触变量 x,并且线程 1/线程 3 不接触同一个原子变量。很明显,如果线程 2 加载 x,它会看到 1,但是该状态是否保证级联到其他线程,这些线程随后与线程 2 进行 acquire/release 同步?或者线程 3 是否也需要获取变量 a 以接收线程 1 对 x 的写入?

根据https://en.cppreference.com/w/cpp/atomic/memory_order

All writes in the current thread are visible in other threads that acquire the same atomic variable

All writes in other threads that release the same atomic variable are visible in the current thread

由于线程 1 和线程 3 不接触同一个原子变量,我不确定仅 acquire/release 是否足以满足上述情况。正式描述中可能隐藏了答案,但我无法完全解决。

*编辑:直到事后才注意到,但在我发布的 link 中有一个示例(“以下示例演示了可传递的发布-获取排序...”)几乎与我的示例相同,但它在所有三个线程中使用相同的原子变量,这似乎很重要。我特意问一下变量不一样的情况


  1. 我是否相信根据标准,必须始终有一对非松弛原子操作,每个线程一个,才能保证任何类型的内存排序?

假设有一个函数“get_data”分配一个缓冲区,向其中写入一些数据,returns 一个指向缓冲区的指针。还有一个函数“use_data”,它接受指向缓冲区的指针并对数据做一些事情。线程 1 从 get_data 获取缓冲区,并使用宽松的原子存储将其传递给线程 2 到全局原子指针。线程 2 在循环中执行宽松的原子加载,直到它获得指针,然后将其传递给 use_data:

int* get_data() {...}
void use_data(int* buf) {...}
int* global_ptr = nullptr;

[Thread 1]
int* buf = get_data();
super_duper_memory_fence();
store_relaxed(global_ptr, buf);

[Thread 2]
int* buf = nullptr;
while ((buf = load_relaxed(global_ptr)) == nullptr);
use_data(buf);

是否有任何类型的操作可以放在“super_duper_memory_fence”中,以保证在use_data获取指针时,缓冲区中的数据也是可见的?据我了解,没有一种可移植的方法来执行此操作,并且线程 2 必须具有匹配的栅栏或其他原子操作,以保证它接收到缓冲区中的写入而不仅仅是指针值。这是正确的吗?

Thread 3's acquire syncs with Thread 2's release, which comes after Thread 2's acquire which syncs with Thread 1's release. Therefore, Thread 3 is guaranteed to see the value that Thread 1 set to x, correct?

是的,这是正确的。 acquire/release 操作建立 同步 关系 - 即 store_release(a)load_acquire(a) 同步,store_release(b) 与 [=13 同步=]. load_acquire(a)store_release(b) 之前排序。 synchronize-withsequenced-before 都是 happens-before 定义的一部分,而 happens -before 关系是可传递的。因此,store_relaxed(x, 1) 发生在 load_relaxed(x).

之前

Am I right in believing that according to the standard, there must always be a pair of non-relaxed atomic operations, one in each thread, in order for any kind of memory ordering at all to be guaranteed?

这个问题有点太宽泛了,但总的来说我倾向于说“是”。通常,在对某些(非原子)共享数据进行操作时,您必须确保存在适当的 happens-before 关系。如果一个线程写入一些共享数据而另一个线程应该读取该数据,则必须确保写入发生在读取之前。有不同的方法来实现这一点——具有正确内存顺序的原子只是一种方法(尽管有人可能会争辩说几乎所有其他方法(如 std::mutex)也归结为原子操作)。

栅栏还必须与其他栅栏或原子操作相结合。如果 super_duper_memory_fence()std::atomic_thread_fence(std::memory_order_release) 并且您在调用 use_data.

之前放置另一个 std::atomic_thread_fence(std::memory_order_acquire),您的示例将起作用

有关更多详细信息,我可以推荐我合着的这篇论文:Memory Models for C/C++ Programmers