需要记忆模型

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==truexy 中的垃圾或两者。

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==2y==3。 **

请注意,编写器和 CPU 仍然允许在写入 x 之前写入 y,类似地,use() 中这些变量的读取可以按任何顺序发生。