我一直在尝试仅使用std C ++构建一个简单(即效率低下)的MPMC队列,但是我很难使底层数组在线程之间进行同步。队列的简化版本是:
constexpr int POISON = 5000;
class MPMC{
std::atomic_int mPushStartCounter;
std::atomic_int mPushEndCounter;
std::atomic_int mPopCounter;
static constexpr int Size = 1<<20;
int mData[Size];
public:
MPMC(){
mPushStartCounter.store(0);
mPushEndCounter.store(-1);
mPopCounter.store(0);
for(int i = 0; i < Size;i++){
//preset data with a poison flag to
// detect race conditions
mData[i] = POISON;
}
}
void push(int x) {
int index = mPushStartCounter.fetch_add(1);
mData[index] = x;//Race condition
atomic_thread_fence(std::memory_order_release);
int expected = index-1;
while(!mPushEndCounter.compare_exchange_strong(expected, index, std::memory_order_acq_rel)){std::this_thread::yield();}
}
int pop(){
int index = mPopCounter.load();
if(index <= mPushEndCounter.load(std::memory_order_acquire) && mPopCounter.compare_exchange_strong(index, index+1, std::memory_order_acq_rel)){
return mData[index]; //race condition
}else{
return pop();
}
}
};
它使用三个原子变量进行同步:
mPushStartCounter
,push(int)
使用它来确定要写入的位置。mPushEndCounter
,用于指示push(int)
已完成写操作,将数组中的内容写入pop()
。mPopCounter
,pop()
使用它来防止出现两次爆裂声。在push()
中,在写入数组mData
和更新mPushEndCounter
之间,我设置了释放屏障,以试图强制mData
数组同步。 / p>
根据我对cpp reference的理解,这将强制执行围栅-原子同步。
push()
中的CAS是一个“原子存储X”,mPushEndCounter
中pop()
的负载是'原子获取操作Y',push()
中的释放屏障“ F”为“在X之前排序”。在这种情况下,cppreference指出
在这种情况下,所有发生在线程A中F之前的非原子和弛豫原子存储都将发生-在来自线程B中Y之后的相同位置的所有非原子和弛豫原子负载发生。
我的解释是,在mData
中可以看到从push()
向pop()
的写操作。但是,情况并非如此,有时pop()
会读取未初始化的数据。我认为这是一个同步问题,因为如果我事后检查队列内容或通过断点检查队列内容,则会读取正确的数据。
我正在使用clang 6.0.1和g ++ 7.3.0。
我尝试查看生成的程序集,但对我来说似乎是正确的:对数组的写入后跟一个lock cmpxchg
,而读取之前对同一个变量进行了检查。据我所知,哪一个应该可以在x64上按预期工作,因为
商店不会与其他商店重新排序,因此cmpxchg
总是排在要排列的商店之后。
lock cmpxchg
刷新写缓冲区,高速缓存等。因此,如果另一个线程将其视为已完成,则可以依靠高速缓存一致性来确保对阵列的写已完成。我不太确定这是正确的。
我在Github上发布了可运行的测试。测试代码涉及16个线程,其中一半将数字0推送到4999,另一半分别读取5000个元素。然后,它结合所有阅读器的结果,并检查我们是否已经正确地查看了[0,4999]中的所有数字8次(失败),并再次扫描基础数组以查看其是否包含[0]中的所有数字。 ,4999] 8次(成功)。