循环数组
对于需要积攒到一定数据量才取出的数据可以使用缓存储存起来,缓存的方式有许多种,这里介绍一个循环数组的缓存形式。
循环数组只是对他处理方式上的叫法,他处理的方式就像一个首尾相接的数组,维护一个头部索引和一个尾部索引。然后一直循环转圈圈的存储和写入数据。
初始化的样子
写入4个数据后的样子
读取2个数据后的样子
然而数组的存储空间在内存中是线性排列的,没有首尾相接的排列方式,这个就需要我们用代码控制来模拟他是可以首尾相接来存取数据。申请6个数据空间,初始化如下图。
初始化
重复上面上面的写入写出部分如下图。
写入4个数据,有效数据为4
读取2个数据,有效数据为2
当再次写入数据并且索引超过最大索引的时候,写入指针就会重新从0开始移动。下图演示了接着往数组里面写入4个数据。
**注意:**除了维护写入索引和读取索引,还需要维护有效数据个数。每次写入的最大数据个数 = 数组长度 - 有效数据个数。
写入1个数据后,写入索引移动到最大值,有效数据为3
写入第2个数据后,写入索引移动到初始0位置继续写入,有效数据为4
4个数据写入完毕,有效数据为6等于数组的长度,此时缓存区满不能再写入数据。
读取数据涉及到最大索引的处理方式是一样的。
下面是代码实现:
public class CircularBuffer
{
//缓存数组
private readonly byte[] buffer;
//锁对象
private readonly object lockObject;
//写入索引
private int writePosition;
//读取索引
private int readPosition;
//有效数据个数
private int byteCount;
public CircularBuffer(int length)
{
buffer = new byte[length];
lockObject = new object();
ResetInner();
}
/// <summary>
/// Number of bytes currently stored in the circular buffer
/// </summary>
public int Count
{
get
{
lock (lockObject)
{
return byteCount;
}
}
}
/// <summary>
/// 写入数据
/// <param name="data">要写入缓存的数组</param>
/// <param name="offset">读取data的起始索引</param>
/// <param name="count">读取多少个数据</param>
/// <returns>缓存了多少个数据</returns>
/// </summary>
public int Write(byte[] data,int offset,int count)
{
lock (lockObject)
{
//表示已经写入的数据
var bytesWritten = 0;
if(count > buffer.Length - byteCount)
{
//最多只能写这么多数据
count = buffer.Length - byteCount;
}
//将从写入头部到数组末尾的长度与要写入的数据长度进行比较,找出最小的,然后进行分段循环从头写入。
int writeToEnd = Math.Min(buffer.Length - writePosition,count);
Array.Copy(data, offset, buffer, writePosition, writeToEnd);
//更新写入索引位置
writePosition += writeToEnd;
writePosition %= buffer.Length;
//更新写入多少数据
bytesWritten += writeToEnd;
//如果写入的数据小于要写入的数量就说明此时索引要从0开始写入
if (bytesWritten < count)
{
//说明一次只写了一部分数据,第二部分数据从头开始写入
Debug.Assert(writePosition == 0);
//写入剩余的数据,count-bytesWritten个数据。
Array.Copy(data,offset+bytesWritten,buffer,writePosition,count-bytesWritten);
//更新写入索引位置(这时候一定不会超过索引最大值)
writePosition += count - bytesWritten;
bytesWritten = count;
}
//有效数据更新
byteCount += bytesWritten;
return bytesWritten;
}
}
/// <summary>
/// 读取数据
/// <param name="data">读取时写入的数组</param>
/// <param name="offset">写入data的起始索引</param>
/// <param name="count">写入多少个数据</param>
/// <returns>写进去了多少个数据</returns>
/// </summary>
public int Read(byte[] data ,int offset,int count)
{
lock (lockObject)
{
if (count > byteCount)
{
count = byteCount;
}
//表示读取了多少个字节
int bytesRead = 0;
//获取第一次读取多少数据。
int readToEnd = Math.Min(buffer.Length - readPosition, count);
Array.Copy(buffer, readPosition, data, offset, readToEnd);
readPosition += readToEnd;
readPosition %= buffer.Length;
bytesRead += readToEnd;
if (readToEnd < count)
{
Debug.Assert(readPosition==0);
Array.Copy(buffer, readPosition, data, offset + bytesRead, count - bytesRead);
readPosition += count - bytesRead;
bytesRead = count;
}
byteCount -= bytesRead;
Debug.Assert(byteCount>=0);
return bytesRead;
}
}
/// <summary>
/// 复位
/// </summary>
public void Reset()
{
lock (lockObject)
{
resetInner();
}
}
/// <summary>
/// 内部复位
/// </summary>
private void resetInner()
{
byteCount = 0;
readPosition = 0;
writePosition = 0;
}
}
复制代码
选用这种方式的原因
只用创建一次够大的数组就可以循环使用,其中的数据也保持了原有的连贯性。
需要注意的
要确保数据再一定周期内可以处理完毕,不会因为处理不及时而积累,最后超过最大数组空间。这里做了最大数据判断,如果超出的数据不会写入到缓存中,换句话说数据直接丢了。