为什么要用环形缓冲区
当有大量数据的时候,我们不能存储所有的数据,那么计算机处理数据的时候,只能先处理先来的,处理之后就会把数据释放掉,再处理下一个。那么已经处理的数据的内存就会被浪费掉。因为后来的数据只能往后排队,如果要将剩余的数据都往前移动一次,那么效率就会低下了,肯定不现实,所以,环形队列就出现了。
环形缓冲区是一项很好的技术,不用频繁的分配内存,而且在大多数情况下,内存的反复使用也使得我们能用更少的内存块做更多的事。
在网络IO线程中,我们会为每一个连接都准备一个环形缓冲区,用于临时存放接收到的数据,以应付半包及粘包的情况。在解包及解密完成后,我们会将这个数据包复制到逻辑线程消息队列中,如果我们只使用一个队列,那这里也将会是个环形缓冲区,IO线程往里写,逻辑线程在后面读,互相追逐。
环形缓冲区的实现原理
环形缓冲区通常有一个读指针和一个写指针。读指针指向环形缓冲区中可读的数据,写指针指向环形缓冲区中可写的缓冲区。通过移动读指针和写指针就可以实现缓冲区的数据读取和写入。在通常情况下,环形缓冲区的读用户仅仅会影响读指针,而写用户仅仅会影响写指针。如果仅仅有一个读用户和一个写用户,那么不需要添加互斥保护机制就可以保证数据的正确性。如果有多个读写用户访问环形缓冲区,那么必须添加互斥保护机制来确保多个用户互斥访问环形缓冲区。
环形缓冲区所有的push和pop操作都是在一个固定 的存储空间内进行。而队列缓冲区在push的时候,可能会分配存储空间用于存储新元素;在pop时,可能会释放废弃元素的存储空间。所以环形方式相比队列方式,少掉了对于缓冲区元素所用存储空间的分配、释放。这是环形缓冲区的一个主要优势。
读写操作
环形缓冲区要维护两个索引,分别对应写入端(W)和读取端(R)。写入(push)的时候,先确保环没满,然后把数据复制到W所对应的元素,最后W指向下一个元素;读取(pop)的时候,先确保环没空,然后返回R对应的元素,最后R指向下一个元素。
判断“空”和“满”
上述的操作并不复杂,不过有一个小小的麻烦:空环和满环的时候,R和W都指向同一个位置!这样就无法判断到底是“空”还是“满”。大体上有两种方法可以解决该问题。
办法1:始终保持一个元素不用
当空环的时候,R和W重叠。当W比R跑得快,追到距离R还有一个元素间隔的时候,就认为环已经满。当环内元素占用的存储空间较大的时候,这种办法显得很土(浪费空间)。
办法2:维护额外变量
如果不喜欢上述办法,还可以采用额外的变量来解决。比如可以用一个整数记录当前环中已经保存的元素个数(该整数>=0)。当R和W重叠的时候,通过该变量就可以知道是“空”还是“满”。
元素的存储
由于环形缓冲区本身就是要降低存储空间分配的开销,因此缓冲区中元素的类型要选好。尽量存储值 类型的数据,而不要存储指针(引用) 类型的数据。因为指针类型的数据又会引起存储空间(比如堆内存)的分配和释放,使得环形缓冲区的效果打折扣。
#define CIRCLEBUFFER_SIZE 8
unsigned char CircleBuffer[CIRCLEBUFFER_SIZE];
unsigned char WriteIndex = 0;
unsigned char ReadIndex = 0;
unsigned char LeftSize = 0;
//检查是否有到数组末尾了
unsigned char Check_CircleBuffer(unsigned char i)
{
return(i + 1) == CIRCLEBUFFER_SIZE ? 0 : i + 1;
}
unsigned char Read_CircleBuffer_Data(void)//读
{
unsigned char Pos;
if (LeftSize > 0)
{
Pos = ReadIndex;
ReadIndex = Check_CircleBuffer(ReadIndex);
LeftSize--;
return CircleBuffer[Pos];
}
return 0;
}
void Write_CircleBuffer_Data(unsigned char Data)//写
{
if (LeftSize < CIRCLEBUFFER_SIZE)
{
CircleBuffer[WriteIndex] = Data;
WriteIndex = Check_CircleBuffer(WriteIndex);
LeftSize++;
}
}
void main(void)
{
Write_CircleBuffer_Data(1);
Read_CircleBuffer_Data();
}