环形队列 Ring Buffer
上一篇降到了环形队列的一些性质。现在来说实现。
从需求开看环形队列需要进行两个操作
- 写入
- 读取
而作为一个大小有限内存区域,就会有临界状态来限制置否能写入成功,或者是成功的取
- 写入-> 如果队列满了就不能写入或者写入失败
- 读取-> 如果队列空时就读取失败或者不能读取
因此总共就至少有4个操作。这里以元素都是int为例,定义一个 RingBuffer。
class RingBuffer
{
public:
RingBuffer();
bool insert(int elem);
bool fetch(int& elem);
bool isEmpty();
bool isFull();
private:
static const int m_size = 5;
int m_RingBufferBuffer[m_size];
int m_wPtr;
int m_rPtr;
};
m_wPtr和m_rPtr记录的是数组m_RingBufferArray的索引。分别用来记录当前操作的位置。这里需要小心的是作为标记位置同时有两种方式
- 表示已经读取或者写入的位置
- 表示下一次将要读取或者写入的位置
根据判断空或者满的判断条件不同,则表示的内容可以不一样。最终能够不会让判断的条件唯一就行了。我们实现的RingBuffer采用标记位置为已经写入的位置和已经读取过的位置。
因此在没有临界条件时。进行插入的逻辑是先移动写指针,然后在进行插入,同理读取也是现需要移动读指针在进行数据读取。
// 插入
m_wPtr = (m_wPtr + 1) % m_size;
m_RingBufferArray[m_wPtr] = elem;
// 读取
m_rPtr = (m_rPtr + 1) % m_size;
elem = m_RingBufferArray[m_rPtr];
如前面提到的,进行操作时需要考虑临界条件。初始状态可以看作为空即读指针和写指针都相等
bool RingBuffer::isEmpty()
{
bool ret = false;
if (m_rPtr == m_wPtr)
{
ret = true;
}
return ret;
}
而对于如果判断为队列满,需要稍稍不同。因此读指针代表已读的位置,假如允许写指针去写入读指针所在的位置就没办法区分到底当前状态为空还是满。
因此需要选择一个不会产生歧义的条件来避免和判空区间发生重复,即当需要写入的下一个位置为读指针所在位置时就判定为满,就不能在写入数据了,当然这就会浪费一格位置,虽然也能通过比方说加入标志位来解决问题,但是同时让逻辑变得复杂了一层。而且实际使用中唤醒队列大小不可能只有5个,因此是可以接受的。如下图,2位置并没有被写入,但是也判定为满。
因此最终我们补全了判空和满的代码
bool RingBuffer::isFull()
{
bool ret = false;
if (((m_wPtr+1) % m_size) == m_rPtr)
{
ret = true;
}
return ret;
}
到这里,环形队列也就是无锁队列的核心数据结构就实现了。最后在构造函数里对状态进行初始化
RingBuffer::RingBuffer()
{
m_wPtr = 0;
m_rPtr = 0;
memset(m_RingBufferArray, 0, sizeof(m_RingBufferArray));
}
针对有两个线程同时操作的数据结构,如果没有同步控制机制就会有并发问题。这涉及到CAS的使用以及内存一致性保护。在下一篇中将针对这两点的实现讲解。