0

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


  1. 获取/释放效果是否保证从一个线程“级联”到下一个线程?

这是我的意思的伪代码示例(假设所有变量都以 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 以保证断言不会触发?我觉得获取/释放就足够了,但我找不到任何简单的解释来保证它。获取/释放的大部分解释主要集中在获取线程接收释放线程所做的所有存储。然而在上面的例子中,线程 2 从未接触过变量 x,而线程 1/线程 3 也不会接触到同一个原子变量。很明显,如果线程 2 要加载 x,它会看到 1,但是该状态是否保证会级联到其他线程,这些线程随后与线程 2 进行获取/释放同步?或者线程 3 是否也需要对变量 a 进行获取,以便接收线程 1 对 x 的写入?

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

当前线程中的所有写入在获取相同原子变量的其他线程中都是可见的

释放相同原子变量的其他线程中的所有写入在当前线程中可见

由于线程 1 和线程 3 没有触及相同的原子变量,我不确定单独的获取/释放是否足以满足上述情况。正式描述中可能隐藏了一个答案,但我无法完全解决。

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


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

想象有一个函数“get_data”,它分配一个缓冲区,向其中写入一些数据,并返回一个指向缓冲区的指针。还有一个函数“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 必须具有匹配的栅栏或其他原子操作,以保证它接收到缓冲区中的写入,而不仅仅是指针值。这个对吗?

4

1 回答 1

2

线程 3 的获取与线程 2 的发布同步,线程 2 的获取与线程 1 的发布同步。因此,线程 3 可以保证看到线程 1 设置为 x 的值,对吗?

是的,这是正确的。获取/释放操作建立同步关系 - 即store_release(a)synchronizes-withload_acquire(a)store_release(b)synchronizes-with load_acquire(b)。并且load_acquire(a)是先排序的store_release(b)synchronize-withsequenced-before都是happens-before定义的一部分,happens-before 关系是传递性的。因此,store_relaxed(x, 1)发生在之前load_relaxed(x)

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

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

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

有关更多详细信息,我可以推荐我与他人合着的这篇论文:C/C++ 程序员的内存模型

于 2020-07-08T09:15:23.687 回答