我目前正在深入研究
std::atomics
和 C++ 内存模型。真正对我的心智模型有帮助的是 CPU 的存储和加载缓冲区的概念,它基本上是一个 fifo 队列,用于必须写入或读取至少存在于英特尔架构中的 L1 缓存的数据。我知道原子操作基本上是给 CPU 的指令,它防止包装类型在编译时或运行时 撕裂 和 跨障碍重新排序写入或读取指令。为了说明我心智模型中的差距,我很快想到了这个例子:
#include <atomic>
#include <iostream>
#include <thread>
int a;
int b;
int c;
std::atomic<int> x;
int e = 0;
auto thread1() {
while(1) {
a = 3;
b = 5;
c = 1;
x.store(10, std::memory_order::release);
e++;
std::cout << "stored!" << std::endl;
}
}
auto thread2() {
while(1) {
x.load(std::memory_order::acquire);
std::cout << b << std::endl;
}
}
int main() {
[[maybe_unused]] auto t1 = std::thread(&thread1);
[[maybe_unused]] auto t2 = std::thread(&thread2);
}
在这里,一个线程写入全局变量 a、b、c、原子变量 x 和普通变量 e(在递增之前读取),而另一个线程从原子变量 x 和普通变量 b 中读取。对于下一部分,假设两个线程实际上都在不同的 CPU 内核上运行。还要记住,这个简单的例子完全忽略了竞争同步,只提供一个静态例子。
现在这是我的心智模型:
正如您所见,存储缓冲区以有序的方式将数据馈送到 L1 缓存中。然后数据通过 L2 和 L3 缓存传播到主内存。没有人知道它什么时候会到达那里,但它会以 64 Kb 的完整缓存行到达(在大多数体系结构上)。现在让我们假设全局变量 a、b、c 碰巧放在与 x 和 e 不同的缓存行上。这引发了我的问题:内存控制器如何知道传播两个缓存行,以便遵守 x 上的原子操作隐含的内存顺序?
我的意思是,如果cacheline 1)恰好在CL 2)之前到达主内存,一切都很好,新写入的a、b和c的值在x的存储之前对其他线程可见。但是,如果发生相反的情况怎么办?如果高速缓存行 2) 首先传播,则对 x 和可能的 e 的写入将在对 a、b 和 c 的写入之前可见,这将导致无效的内存排序。必须以某种方式防止这种情况。我想出了一些可能的解决方案:
可能还有其他我现在想不到的解决方案,但我认为理解这个拼图将帮助我完成我的心理理解,达到可接受的细节数量。如果我的理解有某种缺陷,也请纠正我。
如果我理解 cppreference memory order page 只有原子变量和它依赖的变量保证是可见的。您描述的第二种情况似乎是有效的内存排序。