需要记忆模型
int x, y;
bool ready = false;
void init()
{
x = 2;
y = 3;
ready = true;
}
void use()
{
if (ready)
std::cout << x + y;
}
一个线程调用 init()
函数,而另一个线程(或信号处理程序)调用 use()
函数。人们可能会认为 use()
功能会打印 5
或什么也不做。出于以下几个原因,情况可能并非总是如此:
-
CPU 可能会重新排序
init()
中发生的写入,以便实际执行的代码可能如下所示:void init() { ready = true; x = 2; y = 3; }
-
CPU 可能会重新排序
use()
中发生的读取,以便实际执行的代码可能变为:void use() { int local_x = x; int local_y = y; if (ready) std::cout << local_x + local_y; }
-
优化的 C++编译器可能决定以类似的方式对程序重新排序。
这样的重新排序不能改变在单线程中运行的程序的行为,因为线程不能将调用交错到 init()
和 use()
。另一方面,在多线程设置中,一个线程可能会看到另一个线程执行的部分写操作,其中 use()
可能会看到 ready==true
和 x
或 y
中的垃圾或两者。
C++内存模型允许程序员指定允许哪些重新排序操作,哪些不允许,以便多线程程序也能够按预期运行。上面的例子可以用线程安全的方式重写,如下所示:
int x, y;
std::atomic<bool> ready{false};
void init()
{
x = 2;
y = 3;
ready.store(true, std::memory_order_release);
}
void use()
{
if (ready.load(std::memory_order_acquire))
std::cout << x + y;
}
这里 init()
执行原子存储释放操作。这不仅将值 true
存储到 ready
中,而且还告诉编译器它不能在之前排序的写操作之前移动此操作。
use()
函数执行原子加载 - 获取操作。它读取 ready
的当前值,并禁止编译器在原子加载获取之前放置在其**之后排序的读操作。 ** **
这些原子操作还会使编译器放置所需的任何硬件指令,以通知 CPU 避免不必要的重新排序。
因为原子存储释放与原子加载获取位于相同的内存位置,所以内存模型规定如果加载获取操作看到存储释放操作写入的值,那么 init()
的线程之前执行的所有写入对于 use()
的线程在其加载获取后执行的加载,该存储释放将可见。那就是如果 use()
看到 ready==true
,那么它肯定会看到 x==2
和 y==3
。 **
请注意,编写器和 CPU 仍然允许在写入 x
之前写入 y
,类似地,use()
中这些变量的读取可以按任何顺序发生。