簡體   English   中英

C++多線程原子加載/存儲

[英]c++ multithread atomic load/store

當我閱讀CplusplusConcurrencyInAction一書的第5章時,示例代碼如下,多線程並發加載/存儲一些原子值與momery_order_relaxed。三個數組在每一輪分別保存x、y and z的值。

#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的第三個輸出是(2,0,0) ,此時它讀取x=2 ,並且y=z=0這意味着當y=0x已經等於 2,為什么第三個輸出values2它讀取x=0y=2 ,這意味着 x 是舊值,因為x、y、z正在增加,所以當y=2 ,x 至少為 2。我在我的 PC 上測試代碼,我無法重現那樣的結果。

原因是通過x.load(std::memory_order_relaxed)讀取只能保證您永遠不會在同一線程中看到x減少(在此示例代碼中)。 (它還保證寫入 x 的線程將在下一次迭代中再次讀取相同的值。)

一般來說,不同的線程可以同時從同一個變量中讀取不同的值。 也就是說,不需要所有線程都同意的一致“全局狀態”。 示例輸出應該證明:第一個線程在已經寫入x = 4時可能仍會看到y = 0 ,而第二個線程在已經寫入y = 2時可能仍會看到x = 0 該標准允許這樣做,因為真正的硬件可能以這種方式工作:考慮線程位於不同 CPU 內核上的情況,每個內核都有自己的私有 L1 緩存。

但是,不可能第二個線程看到x = 5然后再看到x = 2原子對象總是保證有一致的全局修改順序(也就是說,觀察到所有對變量的寫入都發生在所有線程的順序相同)。

但是當使用std::memory_order_relaxed ,無法保證線程何時最終“看到”這些寫入*,或者不同線程的觀察結果如何相互關聯。 您需要更強的內存排序才能獲得這些保證。

*事實上,一個有效的輸出將是所有線程一直只讀取0 ,除了寫入線程讀取他們在前一次迭代中寫入的內容到他們的“自己的”變量(其他線程為 0)。 在除非提示否則從不刷新緩存的硬件上,這實際上可能會發生,並且它將完全符合 C++ 標准!

我在我的電腦上測試了代碼,我無法重現那樣的結果。

顯示的“示例輸出”是高度人為的。 C++ 標准允許這種輸出發生。 這意味着您甚至可以在沒有內置緩存一致性保證的硬件上編寫高效且正確的多線程代碼(見上文)。 但是今天的通用硬件(特別是 x86)帶來了很多保證,實際上使某些行為無法觀察(包括問題中的輸出)。

另外,請注意xyz極有可能是相鄰的(取決於編譯器),這意味着它們很可能都位於同一緩存行上。 這將導致性能大幅下降(查找“虛假共享”)。 但是由於內存只能以緩存線粒度在內核之間傳輸,這(連同 x86 一致性保證)使得 x86 CPU(您最有可能使用它執行測試)讀取任何變量的過時值基本上是不可能的。 將這些值分配超過 1-2 個緩存行可能會導致更有趣/混亂的結果。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM