C ++ 11原子内存排序 - 这是放宽(释放 - 消耗)排序的正确用法吗?

时间:2013-03-04 15:03:54

标签: c++ memory c++11 atomic compare-and-swap

我最近使用三重缓冲区的std :: atomic作为并发同步机制,为C ++ 11创建了一个端口。这种线程同步方法背后的想法是,对于生产者 - 消费者情况,你有一个运行更快的生产者,消费者,三重缓冲可以带来一些好处,因为生产者线程不会“因为不得不等待消费者而放慢了速度。在我的例子中,我有一个物理线程,在~120fps时更新,以及一个以~60fps运行的渲染线程。显然,我希望渲染线程始终能够获得最新状态,但我也知道我将从物理线程中跳过很多帧,因为速率不同。另一方面,我希望我的物理线程保持其不变的更新速率,而不受锁定我的数据的较慢渲染线程的限制。

最初的C代码是由remis-ideas制作的,完整的解释在他的blog中。我鼓励任何有兴趣阅读它的人进一步了解原始实现。

我的实施可以找到here

基本思想是在任何给定时间有一个具有3个位置(缓冲区)的数组和一个比较和交换的原子标志,以定义哪些数组元素对应于什么状态。这样,只有一个原子变量用于模拟数组的所有3个索引和三重缓冲背后的逻辑。缓冲区的3个位置被命名为Dirty,Clean和Snap。 producer 总是写入Dirty索引,并且可以翻转writer以将Dirty与当前的Clean索引交换。 使用者可以请求新的Snap,它将当前的Snap索引与Clean索引交换以获取最新的缓冲区。 使用者始终在Snap位置读取缓冲区。

该标志由8位无符号整数组成,位对应于:

(未使用)(新写入)(2x脏)(2x清洁)(2x快照)

newWrite extra bit标志由writer设置并由reader清除。读者可以使用它来检查自上次捕捉以来是否有任何写入,如果不是,则不会再次捕捉。可以使用简单的按位运算获得标志和索引。

现在好了代码:

template <typename T>
class TripleBuffer
{

public:

  TripleBuffer<T>();
  TripleBuffer<T>(const T& init);

  // non-copyable behavior
  TripleBuffer<T>(const TripleBuffer<T>&) = delete;
  TripleBuffer<T>& operator=(const TripleBuffer<T>&) = delete;

  T snap() const; // get the current snap to read
  void write(const T newT); // write a new value
  bool newSnap(); // swap to the latest value, if any
  void flipWriter(); // flip writer positions dirty / clean

  T readLast(); // wrapper to read the last available element (newSnap + snap)
  void update(T newT); // wrapper to update with a new element (write + flipWriter)

private:

  bool isNewWrite(uint_fast8_t flags); // check if the newWrite bit is 1
  uint_fast8_t swapSnapWithClean(uint_fast8_t flags); // swap Snap and Clean indexes
  uint_fast8_t newWriteSwapCleanWithDirty(uint_fast8_t flags); // set newWrite to 1 and swap Clean and Dirty indexes

  // 8 bit flags are (unused) (new write) (2x dirty) (2x clean) (2x snap)
  // newWrite   = (flags & 0x40)
  // dirtyIndex = (flags & 0x30) >> 4
  // cleanIndex = (flags & 0xC) >> 2
  // snapIndex  = (flags & 0x3)
  mutable atomic_uint_fast8_t flags;

  T buffer[3];
};

实现:

template <typename T>
TripleBuffer<T>::TripleBuffer(){

  T dummy = T();

  buffer[0] = dummy;
  buffer[1] = dummy;
  buffer[2] = dummy;

  flags.store(0x6, std::memory_order_relaxed); // initially dirty = 0, clean = 1 and snap = 2
}

template <typename T>
TripleBuffer<T>::TripleBuffer(const T& init){

  buffer[0] = init;
  buffer[1] = init;
  buffer[2] = init;

  flags.store(0x6, std::memory_order_relaxed); // initially dirty = 0, clean = 1 and snap = 2
}

template <typename T>
T TripleBuffer<T>::snap() const{

  return buffer[flags.load(std::memory_order_consume) & 0x3]; // read snap index
}

template <typename T>
void TripleBuffer<T>::write(const T newT){

  buffer[(flags.load(std::memory_order_consume) & 0x30) >> 4] = newT; // write into dirty index
}

template <typename T>
bool TripleBuffer<T>::newSnap(){

  uint_fast8_t flagsNow(flags.load(std::memory_order_consume));
  do {
    if( !isNewWrite(flagsNow) ) // nothing new, no need to swap
      return false;
  } while(!flags.compare_exchange_weak(flagsNow,
                                       swapSnapWithClean(flagsNow),
                                       memory_order_release,
                                       memory_order_consume));
  return true;
}

template <typename T>
void TripleBuffer<T>::flipWriter(){

  uint_fast8_t flagsNow(flags.load(std::memory_order_consume));
  while(!flags.compare_exchange_weak(flagsNow,
                                     newWriteSwapCleanWithDirty(flagsNow),
                                     memory_order_release,
                                     memory_order_consume));
}

template <typename T>
T TripleBuffer<T>::readLast(){
    newSnap(); // get most recent value
    return snap(); // return it
}

template <typename T>
void TripleBuffer<T>::update(T newT){
    write(newT); // write new value
    flipWriter(); // change dirty/clean buffer positions for the next update
}

template <typename T>
bool TripleBuffer<T>::isNewWrite(uint_fast8_t flags){
    // check if the newWrite bit is 1
    return ((flags & 0x40) != 0);
}

template <typename T>
uint_fast8_t TripleBuffer<T>::swapSnapWithClean(uint_fast8_t flags){
    // swap snap with clean
    return (flags & 0x30) | ((flags & 0x3) << 2) | ((flags & 0xC) >> 2);
}

template <typename T>
uint_fast8_t TripleBuffer<T>::newWriteSwapCleanWithDirty(uint_fast8_t flags){
    // set newWrite bit to 1 and swap clean with dirty 
    return 0x40 | ((flags & 0xC) << 2) | ((flags & 0x30) >> 2) | (flags & 0x3);
}

如您所见,我决定使用 Release-Consume 模式进行内存排序。 商店的 Release (memory_order_release)确保当前线程中的写入不能在商店之后重新排序。另一方面, Consume 确保当前线程中的读取不依赖于当前加载的值,可以在之前重新排序。这确保了在当前线程中可以看到对释放相同原子变量的其他线程中的因变量的写入。

如果我的理解是正确的,因为我只需要原子设置标志,所以不会直接影响标志的其他变量的操作可以由编译器自由重新排序,从而允许更多的优化。通过阅读新内存模型上的一些文档,我也意识到这些轻松的原子只会对ARM和POWER等平台产生明显的影响(主要是因为它们而引入)。由于我的目标是ARM,我相信我可以从这些操作中受益,并且能够更多地提高性能。

现在提出问题:

我是否正确使用了针对此特定问题的Release-Consume宽松订购?

谢谢,

安德烈

PS:很抱歉这篇文章很长,但我相信需要一些不错的背景来更好地了解这个问题。

编辑: 实施了@Yakk的建议:

  • 已修复使用直接分配的flagsnewSnap()上的flipWriter()读取,因此使用默认load(std::memory_order_seq_cst)
  • 为了清晰起见,将位操作移动到专用功能。
  • bool返回类型添加到newSnap(),现在当没有新内容时返回false,否则返回true。
  • 使用= delete惯用法将类定义为不可复制的,因为如果使用TripleBuffer,复制和赋值构造函数都是不安全的。

编辑2: 修正了描述,这是不正确的(谢谢@Useless)。 使用者请求新的Snap并从Snap索引(而不是“writer”)读取。抱歉分心,感谢Useless指出它。

编辑3: 根据@Display Name的建议优化了newSnap()flipriter()函数,每个循环周期有效地删除了2个冗余load()

2 个答案:

答案 0 :(得分:2)

为什么要在CAS循环中加载旧标志值两次?第一次是flags.load(),第二次是compare_exchange_weak(),标准在CAS失败时指定将第二次加载到第一个参数,在本例中是flagsNow。

根据http://en.cppreference.com/w/cpp/atomic/atomic/compare_exchange,“否则,将存储在* this中的实际值加载到预期值(执行加载操作)。”那么你的循环正在做什么就是失败时,{ {1}}重新加载compare_exchange_weak(),然后循环重复,第一个语句在加载flagsNow后立即再次加载。在我看来,你的循环应该将负载拉出循环。例如,compare_exchange_weak()将是:

newSnap()

uint_fast8_t flagsNow(flags.load(std::memory_order_consume)); do { if( !isNewWrite(flagsNow)) return false; // nothing new, no need to swap } while(!flags.compare_exchange_weak(flagsNow, swapSnapWithClean(flagsNow), memory_order_release, memory_order_consume));

flipWriter()

答案 1 :(得分:1)

是的,它是memory_order_acquire和memory_order_consume之间的区别,但是当你每秒使用180左右时,你不会注意到它。如果你想知道数字的答案,你可以使用m2 = memory_order_consume运行我的测试。只需将producer_or_consumer_Thread更改为类似的内容:

TripleBuffer <int> tb;

void producer_or_consumer_Thread(void *arg)
{
    struct Arg * a = (struct Arg *) arg;
    bool succeeded = false;
    int i = 0, k, kold = -1, kcur;

    while (a->run)
    {
        while (a->wait) a->is_waiting = true; // busy wait
        if (a->producer)
        {
            i++;
            tb.update(i);
            a->counter[0]++;
        }
        else
        {
            kcur = tb.snap();
            if (kold != -1 && kcur != kold) a->counter[1]++;
            succeeded = tb0.newSnap();
            if (succeeded)
            {
                k = tb.readLast();
                if (kold == -1)
                    kold = k;
                else if (kold = k + 1)
                    kold = k;
                else
                    succeeded = false;
            }
            if (succeeded) a->counter[0]++;   
        }
    }
    a->is_waiting =  true;
}

测试结果:

_#_  __Produced __Consumed _____Total
  1    39258150   19509292   58767442
  2    24598892   14730385   39329277
  3    10615129   10016276   20631405
  4    10617349   10026637   20643986
  5    10600334    9976625   20576959
  6    10624009   10069984   20693993
  7    10609040   10016174   20625214
  8    25864915   15136263   41001178
  9    39847163   19809974   59657137
 10    29981232   16139823   46121055
 11    10555174    9870567   20425741
 12    25975381   15171559   41146940
 13    24311523   14490089   38801612
 14    10512252    9686540   20198792
 15    10520211    9693305   20213516
 16    10523458    9720930   20244388
 17    10576840    9917756   20494596
 18    11048180    9528808   20576988
 19    11500654    9530853   21031507
 20    11264789    9746040   21010829