C++ 多线程 atomic load/store

c++ multithread atomic load/store

看书第5章CplusplusConcurrencyInAction时,示例代码如下,多线程load/store一些原子值并发,用momery_order_relaxed.Three数组保存[=的值13=]分别在每一轮。

#include <thread>
#include <atomic>
#include <iostream>
​
std::atomic<int> x(0),y(0),z(0);  // 1
std::atomic<bool> go(false);  // 2
​
unsigned const loop_count=10;
​
struct read_values
{
  int x,y,z;
};
​
read_values values1[loop_count];
read_values values2[loop_count];
read_values values3[loop_count];
read_values values4[loop_count];
read_values values5[loop_count];
​
void increment(std::atomic<int>* var_to_inc,read_values* values)
{
  while(!go)
    std::this_thread::yield();  
  for(unsigned i=0;i<loop_count;++i)
  {
    values[i].x=x.load(std::memory_order_relaxed);
    values[i].y=y.load(std::memory_order_relaxed);
    values[i].z=z.load(std::memory_order_relaxed);
    var_to_inc->store(i+1,std::memory_order_relaxed);  // 4
    std::this_thread::yield();
  }
}
​
void read_vals(read_values* values)
{
  while(!go)
    std::this_thread::yield(); 
  for(unsigned i=0;i<loop_count;++i)
  {
    values[i].x=x.load(std::memory_order_relaxed);
    values[i].y=y.load(std::memory_order_relaxed);
    values[i].z=z.load(std::memory_order_relaxed);
    std::this_thread::yield();
  }
}
​
void print(read_values* v)
{
  for(unsigned i=0;i<loop_count;++i)
  {
    if(i)
      std::cout<<",";
    std::cout<<"("<<v[i].x<<","<<v[i].y<<","<<v[i].z<<")";
  }
  std::cout<<std::endl;
}
​
int main()
{
  std::thread t1(increment,&x,values1);
  std::thread t2(increment,&y,values2);
  std::thread t3(increment,&z,values3);
  std::thread t4(read_vals,values4);
  std::thread t5(read_vals,values5);
​
  go=true;  
​
  t5.join();
  t4.join();
  t3.join();
  t2.join();
  t1.join();
​
  print(values1);  
  print(values2);
  print(values3);
  print(values4);
  print(values5);
}

本章提到的有效输出之一:

(0,0,0),(1,0,0),(2,0,0),(3,0,0),(4,0,0),(5,7,0),(6,7,8),(7,9,8),(8,9,8),(9,9,10)
(0,0,0),(0,1,0),(0,2,0),(1,3,5),(8,4,5),(8,5,5),(8,6,6),(8,7,9),(10,8,9),(10,9,10)
(0,0,0),(0,0,1),(0,0,2),(0,0,3),(0,0,4),(0,0,5),(0,0,6),(0,0,7),(0,0,8),(0,0,9)
(1,3,0),(2,3,0),(2,4,1),(3,6,4),(3,9,5),(5,10,6),(5,10,8),(5,10,10),(9,10,10),(10,10,10)
(0,0,0),(0,0,0),(0,0,0),(6,3,7),(6,5,7),(7,7,7),(7,8,7),(8,8,7),(8,8,9),(8,8,9)

values1的第3个输出是(2,0,0),此时它是x=2y=z=0。这意味着当y=0时,x 已经等于 2,为什么 values2 的第三个输出显示为 x=0y=2,这意味着 x 是旧值,因为 x、y、z 正在增加,所以当 y=2 时 x 至少为 2。 我在我的电脑上测试了代码,我无法重现那样的结果。

原因是通过 x.load(std::memory_order_relaxed) 读取保证 只有 你永远不会看到 x 在同一个线程中减少(在这个示例代码中)。 (它还保证写入 x 的线程将在下一次迭代中再次读取相同的值。)

一般来说,不同的线程可以同时从同一个变量中读取不同的值。也就是说,不需要所有线程都同意的一致 "global state"。示例输出应该表明:第一个线程在已经写入 x = 4 时可能仍然看到 y = 0,而第二个线程在已经写入 [=15= 时可能仍然看到 x = 0 ].该标准允许这样做,因为真实的硬件可能会以这种方式工作:考虑线程位于不同 CPU 内核上的情况,每个内核都有自己的私有 L1 缓存。

但是,不可能第二个线程看到x = 5,然后又看到x = 2——原子对象总是保证有一个一致的全局修改顺序(即所有的写所有线程都观察到变量以相同的顺序发生。

但是当使用 std::memory_order_relaxed 时,无法保证 什么时候 线程最终会执行 "see" 那些写入*,或者不同线程的观察如何关联对彼此。您需要更强的内存排序才能获得这些保证。

*事实上,一个有效的输出将是所有线程一直只读取 0,除了写入线程读取他们在上一次迭代中写入的内容到他们的 "own"变量(其他为 0)。在没有提示的情况下从不刷新缓存的硬件上,这实际上可能会发生,并且完全符合 C++ 标准!

And I test the code in my PC,I can't reproduce the result like that.

显示的 "example output" 是高度人为的。 C++ 标准 允许 发生此输出。这意味着即使在没有内置缓存一致性保证的硬件上,您也可以编写高效且正确的多线程代码(见上文)。但是今天的普通硬件(特别是 x86)带来了很多保证,实际上使某些行为无法观察(包括问题中的输出)。

另外,请注意 xyz 极有可能相邻(取决于编译器),这意味着它们很可能都落在同一个缓存行上.这将导致性能大幅下降(查找 "false sharing")。但是由于内存只能以缓存行粒度在内核之间传输,这(连同 x86 一致性保证)基本上 不可能 x86 CPU(你最有可能执行你的测试)读取任何变量的过时值。将这些值分配超过 1-2 个缓存行可能会导致更多 interesting/chaotic 结果。