我正在尝试实现环形缓冲区(或循环缓冲区)。与大多数这些实现一样,它应该尽可能快速和轻量级,但仍然提供足够的安全性以足够强大以供生产使用。这是一个难以平衡的罢工。特别是我遇到了以下问题。
我想使用所述缓冲区来存储最后n个系统事件。随着新事件的进入,最早被删除。然后,我的软件的其他部分可以访问这些存储的事件并按照自己的进度处理它们。有些系统可能消耗的事件几乎与它们到达时一样快,其他系统可能只是偶尔检查。每个系统都会将一个迭代器存储到缓冲区中,以便它们在上次检查时知道它们停在哪里。这是没有问题的,只要他们经常检查足够多,但特别是较慢的系统可能经常发现自己使用旧的迭代器指向缓冲元素,该缓冲元素已被覆盖而无法检测到。
是否有一种好的(不太昂贵的)检查任何给定迭代器是否仍然有效的方法?
到目前为止我想出的事情:
许多环形缓冲区实现根本不打扰这个或使用单读取单一写法习惯,其中读取正在删除。
答案 0 :(得分:4)
不存储值,而是存储(value, sequence_num)
对。当您推送新的value
时,请务必确保它使用不同的sequence_num
。您可以对sequence_num
使用单调递增的整数。
然后,迭代器会记住它最后查看的元素的sequence_num
。如果不匹配,则会被覆盖。
答案 1 :(得分:2)
Roger Lipscombe's answer的变体,是使用序列号作为迭代器。序列号应该单调递增(当整数类型溢出时要特别注意)固定步长(例如1)。
循环缓冲区本身会正常存储数据,并会跟踪它当前包含的最旧序列号(在尾部位置)。
取消引用迭代器时,将根据缓冲区最早的序列号检查迭代器的序列号。如果它更大或相等(再次特别注意整数溢出),则可以使用简单的索引计算来检索数据。如果它更小,则表示数据已被覆盖,应该检索当前的尾部数据(相应地更新迭代器的序列号)。
答案 2 :(得分:2)
我同意Roger Lipscombe,使用序列号。
但是您不需要存储(value,sequence_num)对:只需存储值,并跟踪到目前为止最高的序列号。由于它是一个环形缓冲区,您可以推导出所有条目的序列号。
因此,迭代器只包含一个序列号。
鉴于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 ;
}
}