环形数组(ringbuffer)

一:环形数组

1、概念

**百度百科:**是一种用于表示一个固定尺寸、头尾相连的缓冲区的数据结构,适合缓存数据流
**个人理解:**本身就是一个定长数组,在存储数据时,达到存储上限时会从0继续存储,也就是他在存储数据时是个闭环的过程,举个例子:如下图,假设是个 大小为9字节的数组,当存满了9个字节的数据时,在存入字节时就会从0开始存储,环形数组也称ringbuffer
环形数组(ringbuffer)_第1张图片

2、优势

首先是一个先进先出的队列,当取走数据时不需要移动数组中的其他元素,在单消费者单生产者的模型下,不需要加锁,可以更快的存取数据。

3、实现

首先定义一个Ringbuffer的类,成员变量定义如下

 	unsigned char*m_pBuffer; //数组指针
    unsigned int m_nSize;    //数组大小
    unsigned int m_w_index;  //写索引    
    unsigned int m_r_index;  //读索引

成员函数申明如下(主要了解存取):

    unsigned int Put(const uint8_t *pBuffer, unsigned int nLen);
    unsigned int Get(uint8_t *pBuffer, unsigned int nLen);

以下图为参考来理解一个ringbuffer的实现,图中r_idx 为读索引,w_idx为写索引,大小为14, 当前为ringbuffer的初始状态。
环形数组(ringbuffer)_第2张图片
举个例子:当存9个数据,取2个数据时,索引为下图状态
环形数组(ringbuffer)_第3张图片
写数据分析
为了去除不必要的边界判断,加入2个前提
1、写入的数据长度大于ringbuffer可写入的长度,则忽略,也就是禁止写入。
2、如果数据长度 >=数组长度(达到容量上限)则禁止写入。
假设写入一个任意长度的数组,那么可能存在如下情况
情况1:写索引>=读索引,待写数据长 <= “右可写入部分”的长度,直接写入整个待写的数据
情况2:写索引>=读索引,待写数据长 > “右可写入部分”的长度,先写入右可写的长度,在把剩下的写入做可写部分环形数组(ringbuffer)_第4张图片情况3:写索引<读索引,直接写入整个待写的数据环形数组(ringbuffer)_第5张图片
写数据代码实现

/**
*	buffer: 待存入的数据指针
*	len: 待存入的数据长度
*/

unsigned int RingBuffer::Put(const unsigned char *buffer, unsigned int len)
{
    unsigned int cur_r = m_r_index;
    unsigned int datalen  = m_w_index - cur_r;
    //已经满了
    if(datalen >= m_nSize)
        return 0;
    //索引在数组的位置
    unsigned int t_w  = m_w_index   & (m_nSize - 1);
    unsigned int t_r = cur_r & (m_nSize - 1);
    if(t_w >= t_r){
    	//情况1
        if(len <= (m_nSize-t_w)){
            memcpy(m_pBuffer + t_w, buffer, len);
        }else{//情况1
            memcpy(m_pBuffer + t_w, buffer, (m_nSize-t_w));
            memcpy(m_pBuffer , buffer, len - (m_nSize-t_w));
        }
    }else{
    	//情况3
         memcpy(m_pBuffer + t_w , buffer, len);
    }
    m_w_index += len;
    return len;
}

读数据分析
同样的去除不必要的边界判断,加入2个前提:
1、数据长度 < 待读入的长度,则忽略
2、数据长度为0时(无数据可读),忽略
情况1:写索引>读索引,直接读取
环形数组(ringbuffer)_第6张图片
情况2:写索引 <= 读索引,待读数据长 <= “右可读”的长度,直接读取待读长度
情况2:写索引 <= 读索引,待读数据长 > “右可读”的长度,先读取“右可读”的数据,在从“左可读”读取剩余部分
环形数组(ringbuffer)_第7张图片
读数据实现

unsigned int RingBuffer::Get(unsigned char *buffer, unsigned int len)
{
    unsigned int cur_w = m_w_index;
    unsigned int datalen  = cur_w - m_r_index;
    //空了或小于
    if(datalen < len || datalen ==0 )
        return 0;
   
    //索引在数组的位置
    unsigned int t_w  = cur_w & (m_nSize - 1);
    unsigned int t_r = m_r_index & (m_nSize - 1);
    if(t_w > t_r){
        memcpy(buffer,m_pBuffer + t_r, len);
    }else{
            if(len <=  (m_nSize - t_r))
                memcpy(buffer,m_pBuffer + t_r, len);
            else{
                memcpy(buffer,m_pBuffer + t_r, (m_nSize - t_r));
                memcpy(buffer+(m_nSize - t_r),m_pBuffer, len - (m_nSize - t_r));
            }
    }
    m_r_index += len;
    return len;
}

不加锁分析
现在考虑2个线程,一个读一个写
当写数据时,我们会先去获取读索引的值,那么在读线程就有可能改变读索引(读索引只会变大,不会变小),此时的写索引在没获取到读索引值时是保持不变的,那么读索引不可能超过写索引的值的(读线程有判断),所以读索引的值改变范围始终会在下图箭头标注的区间,因此不管什么情况我们会在这个区间内拿到一个读索引,我们就得到一个可写入的数据区间,然后忘里面写数据,假设在写的过程读线程又来修改读索引,怎么办?此时已经无需关心读索引了,因为在此之前我们拿到了一个读索引副本,此后所有的读索引都比这个副本值大,此时无论我们怎么写入这个空间都不会影响到读线程去读数据
同理读数据不加锁分析也一样
环形数组(ringbuffer)_第8张图片
多线程就不行了,假如2个线程同时写就有可能对一个地方重复写入

gitHub源码

你可能感兴趣的:(linux,后台学习笔记,c++)