使用原子变量和内存栅栏同步非原子数组

时间:2018-08-12 16:50:10

标签: c++ c++11 concurrency synchronization

我一直在尝试仅使用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();
      }
    }
  };

它使用三个原子变量进行同步:

  • mPushStartCounterpush(int)使用它来确定要写入的位置。
  • mPushEndCounter,用于指示push(int)已完成写操作,将数组中的内容写入pop()
  • mPopCounterpop()使用它来防止出现两次爆裂声。

push()中,在写入数组mData和更新mPushEndCounter之间,我设置了释放屏障,以试图强制mData数组同步。 / p>

根据我对cpp reference的理解,这将强制执行围栅-原子同步。

  • push()中的CAS是一个“原子存储X”,
  • mPushEndCounterpop()的负载是'原子获取操作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次(成功)。

0 个答案:

没有答案