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=2
和y=z=0
。这意味着当y=0
时,x
已经等于 2,为什么 values2
的第三个输出显示为 x=0
和 y=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)带来了很多保证,实际上使某些行为无法观察(包括问题中的输出)。
另外,请注意 x
、y
和 z
极有可能相邻(取决于编译器),这意味着它们很可能都落在同一个缓存行上.这将导致性能大幅下降(查找 "false sharing")。但是由于内存只能以缓存行粒度在内核之间传输,这(连同 x86 一致性保证)基本上 不可能 x86 CPU(你最有可能执行你的测试)读取任何变量的过时值。将这些值分配超过 1-2 个缓存行可能会导致更多 interesting/chaotic 结果。
看书第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=2
和y=z=0
。这意味着当y=0
时,x
已经等于 2,为什么 values2
的第三个输出显示为 x=0
和 y=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)带来了很多保证,实际上使某些行为无法观察(包括问题中的输出)。
另外,请注意 x
、y
和 z
极有可能相邻(取决于编译器),这意味着它们很可能都落在同一个缓存行上.这将导致性能大幅下降(查找 "false sharing")。但是由于内存只能以缓存行粒度在内核之间传输,这(连同 x86 一致性保证)基本上 不可能 x86 CPU(你最有可能执行你的测试)读取任何变量的过时值。将这些值分配超过 1-2 个缓存行可能会导致更多 interesting/chaotic 结果。