繁体   English   中英

在这个无锁 SPSC 环形缓冲区队列中,每个原子的 memory 订单是否正确?

[英]Are memory orders for each atomic correct in this lock-free SPSC ring buffer queue?

我有一个看起来像这样的环形缓冲区:

template<class T>
class RingBuffer {
public:
  bool Publish();
 
  bool Consume(T& value);

  bool IsEmpty(std::size_t head, std::size_t tail);

  bool IsFull(std::size_t head, std::size_t tail);

private:
  std::size_t Next(std::size_t slot);

  std::vector<T> buffer_;
  std::atomic<std::size_t> tail_{0};
  std::atomic<std::size_t> head_{0};
  static constexpr std::size_t kBufferSize{8};
};

此数据结构旨在与两个线程一起使用:发布者线程和消费者线程。 没有通过 memory 命令的两个主要函数到下面列出的原子:

bool Publish(T value) {
    const size_t curr_head = head_.load(/* memory order */);
    const size_t curr_tail = tail_.load(/* memory_order */);

    if (IsFull(curr_head, curr_tail)) {
      return false;
    }

    buffer_[curr_tail] = std::move(value);
    tail_.store(Next(curr_tail) /*, memory order */);
    return true;
  }

bool Consume(T& value) {
    const size_t curr_head = head_.load(/* memory order */);
    const size_t curr_tail = tail_.load(/* memory order */);

    if (IsEmpty(curr_head, curr_tail)) {
      return false;
    }

    value = std::move(buffer_[curr_head]);
    head_.store(Next(curr_head) /*, memory order */);
    return true;
  }

我知道,至少,我必须在Publish() function 中有tail_.store()std::memory_order::releasetail_.load()std::memory_order::acquireConsume() function 到create发生在buffer_的写入和读取buffer_之间的连接之前。 此外,我可以将std::memory_order::relaxed传递给Publish() tail_.load()head_.load() ) 和Consume() () 中的 head_.load(),因为同一个线程将看到最后一次写入原子。 现在函数是这样的:

 bool Publish(T value) {
     const size_t curr_head = head_.load(/* memory order */);
     const size_t curr_tail = tail_.load(std::memory_order::relaxed);
    
     if (IsFull(curr_head, curr_tail)) {
         return false;
     }
    
     buffer_[curr_tail] = std::move(value);
     tail_.store(Next(curr_tail), std::memory_order::release);
     return true;
 }

 bool Consume(T& value) {
     const size_t curr_head = head_.load(std::memory_order::relaxed);
     const size_t curr_tail = tail_.load(std::memory_order::acquire);
    
     if (IsEmpty(curr_head, curr_tail)) {
         return false;
     }
    
     value = std::move(buffer_[curr_head]);
     head_.store(Next(curr_head) /*, memory order */);
     return true;
 }

最后一步是将 memory 订单放入剩余的一对: head_.load()中的 head_.load( Publish()和 Consume() 中的head_.store() Consume() 我必须有value = std::move(buffer_[curr_head]); Consume() () 中的head_.store()之前执行的行,否则在缓冲区已满的情况下会出现数据竞争,因此,至少,我必须将std::memory_order::release传递给该存储操作以避免重新排序。 但是我是否必须将std::memory_order::acquire放入Publish() function 中的head_.load()中? 我知道这将有助于head_.load() head_.store()std::memory_order::relaxed不同,但如果我不需要这种更短时间的保证来查看商店的副作用操作,我可以有一个轻松的 memory 订单吗? 如果我不能,那为什么? 完成代码:

bool Publish(T value) {
    const size_t curr_head = head_.load(std::memory_order::relaxed); // or acquire?
    const size_t curr_tail = tail_.load(std::memory_order::relaxed);
        
    if (IsFull(curr_head, curr_tail)) {
        return false;
    }
        
    buffer_[curr_tail] = std::move(value);
    tail_.store(Next(curr_tail), std::memory_order::release);
    return true;
}
    
bool Consume(T& value) {
    const size_t curr_head = head_.load(std::memory_order::relaxed);
    const size_t curr_tail = tail_.load(std::memory_order::acquire);
        
    if (IsEmpty(curr_head, curr_tail)) {
        return false;
    }
        
    value = std::move(buffer_[curr_head]);
    head_.store(Next(curr_head), std::memory_order::release);
    return true;
}

每个原子的 memory 订单是否正确? 我对在每个原子变量中使用每个 memory 顺序的解释是否正确?

以前的答案可能有助于作为背景:
c++,std::atomic,什么是 std::memory_order 以及如何使用它们?
https://bartoszmilewski.com/2008/12/01/c-atomics-and-memory-ordering/

首先,您描述的系统称为单一生产者 - 单一消费者队列。 您可以随时查看此容器的增强版本进行比较。 我经常会检查 boost 代码,即使我在不允许 boost 的情况下工作。 这是因为检查和理解一个稳定的解决方案会让您深入了解您可能遇到的问题(他们为什么这样做?哦,我明白了 - 等等)。 鉴于您的设计,并且编写了许多类似的容器,我会说您的设计必须小心区分空与满。 如果您使用经典的 {begin,end} 对,您会遇到由于换行导致的问题

{开始,开始+大小} == {开始,开始} == 空

好的,回到同步问题。

鉴于顺序仅影响重新排序,因此在 Publish 中使用 release 似乎是对标志的教科书式使用。 在容器的大小增加之前,什么都不会读取该值,因此您不关心值本身的写入顺序是否以随机顺序发生,您只关心在计数增加之前必须完全写入该值. 所以我同意,您正确使用了 Publish function 中的标志。
我确实质疑消费中是否需要“释放”,但如果您要退出队列,并且这些移动具有副作用,则可能需要。 我想说,如果您追求原始速度,那么可能值得制作第二个版本,专门用于琐碎的对象,它使用宽松的顺序来增加头部。

您也可以在推送/弹出时考虑就地新建/删除。 虽然大多数移动会将 object 留在空的 state 中,但标准只要求在移动后将其留在有效的 state 中。 移动后显式删除 object 可能会避免您以后出现模糊的错误。

您可能会争辩说,consume 中的两个原子负载可能是 memory_order_consume。 这放宽了约束,说“我不在乎它们的加载顺序,只要它们在使用时都被加载”。 尽管我怀疑在实践中它会产生任何收益。 我也对这个建议感到紧张,因为当我查看增强版本时,它与您所拥有的非常接近。 https://www.boost.org/doc/libs/1_66_0/boost/lockfree/spsc_queue.hpp

 template <typename Functor>
    bool consume_one(Functor & functor, T * buffer, size_t max_size)
    {
        const size_t write_index = write_index_.load(memory_order_acquire);
        const size_t read_index  = read_index_.load(memory_order_relaxed);
        if ( empty(write_index, read_index) )
            return false;

        T & object_to_consume = buffer[read_index];
        functor( object_to_consume );
        object_to_consume.~T();

        size_t next = next_index(read_index, max_size);
        read_index_.store(next, memory_order_release);
        return true;
    }

一般来说,尽管您的方法看起来不错并且与 boost 版本非常相似。 但是......如果我是你,我可能会逐行通过 boost 版本 go ,看看它有什么不同。 很容易犯错。

编辑:抱歉,我刚刚注意到您的代码中 memory_order_acquire/memory_order_relaxed 标志的顺序错误。 你应该让最后一个写一个“发布”,第一个读“获取”。 这不会显着影响行为,但它更容易阅读。 (开始同步,结束同步)

回复评论:正如@user17732522所暗示的,复制操作也是写,所以对琐碎对象的优化不会同步,对琐碎的object的优化会引入U/B(妈的容易出错)

为了理解这个问题的正确std::memory_order ,让我们考虑一下是否只有一个线程而不是生产者和消费者线程。
在单线程场景中bool Publish(T value)将看到之前bool Consume(T& value)执行的所有操作以相同的顺序执行,类似地bool Consume(T& value)将看到之前bool Publish(T value)执行的所有操作以相同的顺序执行.
所以在多线程场景中,发布者和消费者线程必须以类似的方式同步。 同步可以通过memory barriers来实现。

消费 function release原子存储head_.store(Next(curr_head), std::memory_order::release); 保证之前发布到store的操作将在它之前执行并且不会越过它,并且可以通过acquire head_变量的原子加载来同步发布,并且保证如果发布可以看到head_它将看到在它之前执行的所有操作,

bool Publish(T value) {
    const size_t curr_head = head_.load(std::memory_order::acquire);
    const size_t curr_tail = tail_.load(std::memory_order::relaxed);
        
    if (IsFull(curr_head, curr_tail)) {
        return false;
    }
        
    buffer_[curr_tail] = std::move(value);
    tail_.store(Next(curr_tail), std::memory_order::release);
    return true;
}

同样,Consume 可以与 Publish 同步release原子存储tail_.store(Next(curr_tail), std::memory_order::release); 通过acquire tail_变量的原子负载,如果consume可以看到tail_ ,则可以保证看到之前执行的所有操作。

bool Consume(T& value) {
    const size_t curr_tail = tail_.load(std::memory_order::acquire);
    const size_t curr_head = head_.load(std::memory_order::relaxed);
        
    if (IsEmpty(curr_head, curr_tail)) {
        return false;
    }
        
    value = std::move(buffer_[curr_head]);
    head_.store(Next(curr_head), std::memory_order::release);
    return true;
}

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM