C++ 使用原子的无锁单生产者多消费者数据结构
我最近有如下示例代码(实际代码要复杂得多)。看了Hans Boehm关于atomic的cppcon16演讲后,我有点担心我的代码是否有效C++ 使用原子的无锁单生产者多消费者数据结构,c++,c++14,lock-free,stdatomic,C++,C++14,Lock Free,Stdatomic,我最近有如下示例代码(实际代码要复杂得多)。看了Hans Boehm关于atomic的cppcon16演讲后,我有点担心我的代码是否有效 product由单个生产者线程调用,consume由多个消费者线程调用。生产者只更新序列号中的数据,如2,4,6,8,但是设置为奇数序列num,比如1,3,5,7。。。在更新数据之前,指示数据可能已脏。消费者也尝试以相同的顺序(2、4、6,…)获取数据 使用者在读取后仔细检查序列num,以确保数据良好(在读取期间生产者未更新数据) 我认为我的代码在x86_64
product
由单个生产者线程调用,consume
由多个消费者线程调用。生产者只更新序列号中的数据,如2,4,6,8,但是设置为奇数序列num,比如1,3,5,7。。。在更新数据之前,指示数据可能已脏。消费者也尝试以相同的顺序(2、4、6,…)获取数据
使用者在读取后仔细检查序列num,以确保数据良好(在读取期间生产者未更新数据)
我认为我的代码在x86_64(我的目标平台)上运行得很好,因为x86_64不使用其他存储重新排序存储,也不使用存储或加载进行加载,但我怀疑这在其他平台上是错误的
我是否可以将数据分配(在产品中)移动到“存储(n-1)”上方,以便消费者读取损坏的数据,但t==t2
仍然成功
struct S
{
atomic<int64_t> seq;
// data members of primitive type int, double etc
...
};
S s;
void produce(int64_t n, ...) // ... for above data members
{
s.seq.store(n-1, std::memory_order_release); // indicates it's working on data members
// assign data members of s
...
s.seq.store(n, std::memory_order_release); // complete updating
}
bool consume(int64_t n, ...) // ... for interested fields passed as reference
{
auto t = s.load(std::memory_order_acquire);
if (t == n)
{
// read fields
...
auto t2 = s.load(std::memory_order_acquire);
if (t == t2)
return true;
}
return false;
}
结构
{
原子序数;
//基本类型为int、double等的数据成员
...
};
S S;
无效产品(int64_t n,…)/。。。对于上述数据成员
{
s、 seq.store(n-1,std::memory_order_release);//表示它正在处理数据成员
//分配s的数据成员
...
s、 seq.store(n,std::memory_order_release);//完成更新
}
布尔消耗(int64_t n,…)/。。。对于作为引用传递的感兴趣的字段
{
自动t=s.load(标准::内存\u顺序\u获取);
如果(t==n)
{
//读取字段
...
自动t2=s.load(标准::内存\u顺序\u获取);
如果(t==t2)
返回true;
}
返回false;
}
当X86瞄准X86时,它仍然会咬你,因为编译器优化了C++程序中的程序的行为,而不是依赖于更强的体系结构依赖行为。由于我们希望避免内存顺序\u seq\u cst
,因此允许重新排序
是的,您的商店可以按照您的建议重新订购。您的加载还可以使用t2
load重新排序,因为。编译器完全优化t2检查是合法的。如果可以进行重新排序,编译器就可以确定这是经常发生的事情,并应用“好像”规则来生成更高效的代码。(当前的编译器通常不允许这样做,但当前编写的标准肯定允许这样做。请参阅。)
防止重新排序的选项包括:
- 使用release和acquire语义使所有数据成员存储/加载原子化。(最后一个数据成员的获取加载将保持
t2
加载不会首先完成。)
- 用于将所有非原子存储和非原子加载作为一个组进行订购。
正如杰夫·普雷辛所解释的,这是我们需要的一种双向屏障。atomic只是循环使用std::mo_u名称,而不是为围栏指定不同的名称
(顺便说一句,非原子存储/加载实际上应该是原子的,
mou-released
,因为在重写过程中读取它们在技术上是未定义的行为,即使您决定不查看所读取的内容。)
请注意额外的编译器屏障(信号栅栏仅影响编译时重新排序),以确保编译器不会将一次迭代中的第二个存储与下一次迭代中的第一个存储合并(如果在循环中运行)。或者更一般地说,确保使区域无效的存储尽可能晚地完成,以减少误报。(对于真正的编译器来说可能不是必需的,并且调用此函数之间有大量的代码。但是signal\u fence从不编译为任何指令,似乎比将第一个存储保持为mo\u release
更好。在版本存储线程fence都编译为额外指令的架构上,一个宽松的store避免使用两个单独的屏障说明。)
我还担心第一个存储与上一次迭代中的发布存储重新排序的可能性。但我认为这种情况永远不会发生,因为两家商店都在同一个地址。(在编译时,可能标准允许恶意编译器执行此操作,但任何理智的编译器如果认为其中一个可以传递另一个,则根本不会执行其中一个存储。)在弱有序体系结构上的运行时,我不确定相同地址的存储是否会变得无序这在现实生活中不应该是一个问题,因为制片人可能不会被连续调用。
顺便说一句,您使用的同步技术是一个,但只有一个写入程序。您只有序列部分,而没有锁部分来同步不同的写入程序。在多编写器版本中,编写器将在读取/写入序列号和数据之前锁定。(而不是将seq no作为函数arg,而是从锁中读取它)
C++标准讨论论文(关于原子的编译器优化,请参阅我回答的后半部分)将其用作示例
它们使用writer中数据项的发布存储,而不是StoreStore围栏。(对于原子数据项,正如我所提到的,这是真正正确的要求)
他们讨论让读取器的第二次序列号加载可能在以后的操作中重新排序,如果编译器这样做有利的话,并在读取器中使用t2=seq.fetch\u add(0,std::memory\u order\u release)
作为获得具有释放语义的加载的潜在方法。对于目前的编译器,我不建议这样做;您可能会在x86上得到一个lock
ed操作,而我上面建议的方法没有任何(或任何实际的屏障说明
void produce(int64_t n, ...) // ... for above data members
{
/*********** changed lines ************/
std::atomic_signal_fence(std::memory_order_release); // compiler-barrier to make sure the compiler does the seq store as late as possible (to give the reader more time with it valid).
s.seq.store(n-1, std::memory_order_relaxed); // changed from release
std::atomic_thread_fence(std::memory_order_release); // StoreStore barrier prevents reordering of the above store with any below stores. (It's also a LoadStore barrier)
/*********** end of changes ***********/
// assign data members of s
...
// release semantics prevent any preceding stores from being delayed past here
s.seq.store(n, std::memory_order_release); // complete updating
}
bool consume(int64_t n, ...) // ... for interested fields passed as reference
{
if (n == s.seq.load(std::memory_order_acquire))
{
// acquire semantics prevent any reordering with following loads
// read fields
...
/*********** changed lines ************/
std::atomic_thread_fence(std::memory_order_acquire); // LoadLoad barrier (and LoadStore)
auto t2 = s.seq.load(std::memory_order_relaxed); // relaxed: it's ordered by the fence and doesn't need anything extra
// std::atomic_signal_fence(std::memory_order_acquire); // compiler barrier: probably not useful on the load side.
/*********** end of changes ***********/
if (n == t2)
return true;
}
return false;
}
void writer(T d1, T d2) {
unsigned seq0 = seq.load(std::memory_order_relaxed); // note that they read the current value because it's presumably a multiple-writers implementation.
seq.store(seq0 + 1, std::memory_order_relaxed);
data1.store(d1, std::memory_order_release);
data2.store(d2, std::memory_order_release);
seq.store(seq0 + 2, std::memory_order_release);
}