检测环形缓冲区的无效迭代器

detecting invalid iterators for a ring buffer

本文关键字:无效 迭代器 缓冲区 检测      更新时间:2023-10-16

我正在尝试实现一个环形缓冲区(或循环缓冲区)。与这些实现中的大多数一样,它应该尽可能快、尽可能轻,但仍然提供足够的安全性,足以用于生产。这是一个很难达成的平衡。我尤其面临以下问题。

我想使用所述缓冲区来存储最后n个系统事件。随着新事件的出现,最旧的事件将被删除。然后,我的软件的其他部分可以访问这些存储的事件,并按照自己的节奏进行处理。一些系统可能会以事件到达的速度消耗事件,而另一些系统可能只是偶尔检查。每个系统都会在缓冲区中存储一个迭代器,这样他们就知道上次检查时停在了哪里。只要他们检查得足够频繁,这就没有问题,但尤其是速度较慢的系统可能经常发现自己有一个旧的迭代器,它指向一个缓冲区元素,该元素后来被覆盖,却没有办法检测到。

有没有一种好的(不太昂贵的)方法来检查任何给定的迭代器是否仍然有效?

到目前为止我想到的东西:

  • 保留所有迭代器的列表并存储它们的有效状态(相当昂贵)
  • 不仅在调用系统中存储迭代器,而且在缓冲区的客户端中存储指向元素的副本。在每次访问时,检查元素是否仍然相同。这可能是不可靠的。如果元素已被相同的元素覆盖,则无法检查它是否已更改。此外,找到检查元素的好方法的责任在于客户,这在我看来并不理想

许多环形缓冲区实现根本不关心这一点,或者使用单读单写习惯用法,即读取即删除。

不存储值,而是存储(value, sequence_num)对。推送新的value时,请始终确保它使用不同的sequence_num。对于sequence_num,可以使用单调递增的整数。

然后,迭代器会记住它最后一次查看的元素的sequence_num。如果不匹配,它就会被覆盖。

我同意Roger Lipscombe的观点,使用序列号。

但您不需要存储(value,sequence_num)对:只需存储值,并跟踪到目前为止的最高序列号。由于它是一个环形缓冲区,您可以推断出所有条目的seqnum。

因此,迭代器只是由一个序列号组成。

假设Obj是您存储在环形缓冲区中的对象类型,如果您使用一个简单的数组,您的环形缓冲区将如下所示:

struct RingBuffer {
    Obj buf[ RINGBUFFER_SIZE ] ;
    size_t idx_last_element ;
    uint32_t seqnum_last_element ;
    void Append( const Obj& obj ) { // TODO: Locking needed if multithreaded 
        if ( idx_last_element == RINGBUFFER_SIZE - 1 )
            idx_last_element = 0 ; 
        else 
            ++idx_last_element ;
        buf[ idx_last_element ] = obj ; // copy.
        ++ seqnum_last_element ;
    }
}

迭代器看起来是这样的:

struct RingBufferIterator {
    const RingBuffer* ringbuf ;
    uint32_t seqnum ;
    bool IsValid() { 
        return ringbuf && 
               seqnum <= ringbuf->seqnum_last_element &&
               seqnum > ringbuf->seqnum_last_element - RINGBUFFER_SIZE ; //TODO: handle seqnum rollover.
    }
    Obj* ToPointer() {
         if ( ! IsValid() ) return NULL ;
         size_t idx = ringbuf->idx_last_element - (ringbuf->seqnum_last_element-seqnum) ; //TODO: handle seqnum rollover.
         // handle wrap around:
         if ( idx < 0 ) return ringbuf->buf + RINGBUFFER_SIZE- idx ;
         return ringbuf->buf + idx ;
   }
}

Roger Lipscombe答案的一个变体是使用序列号作为迭代器。序列号应该是单调递增的(当整数类型溢出时要特别小心),有一个固定的步长(例如1)

循环缓冲区本身将正常存储数据,并跟踪其当前包含的最旧序列号(位于尾部位置)。

当取消引用迭代器时,迭代器的序列号将与缓冲区最旧的序列号进行检查。如果它大于或等于(同样要特别注意整数溢出),则可以使用简单的索引计算来检索数据。如果它较小,则意味着数据已被覆盖,而应该检索当前的尾部数据(相应地更新迭代器的序列号)。