队列
概念
队列是模拟一组人排队办事行为的数组结构
。
队列的直观体验:https://www.cs.usfca.edu/~galles/visualization/QueueArray.html
顺序队列
队列采用顺序存储结构
时,可利用一维数组来存放节点数据,在数组中ucArray[]
中存放队列。
head与tail的引入
由于队列的操作只能在队头和队尾上
进行,且不能移动队列中的节点
,因此必须有活动的队头和队尾的索引。
图中ucHead
为头索引,指向一个满节点
;ucTail
为尾索引,指向一个空节点
。
用C语言描述
#define MAXSIZE (128) //队列的最大长度
typedef struct _queue_t_
{
unsigned char ucArray[MAXSIZE]; //所用的内存空间
unsigned char ucHead; //队头索引
unsigned char ucTail; //队尾索引
}queue_t;
出队时
ucHead ++;
入队时
ucArray[ucTail ++] = x;
不足
当其入队到最后时,要做一次整体搬移动作,以腾出空间。
数据搬移用时会较长,影响入队速度。
而循环队列则不会出现此问题
循环队列(ring-buffer)
什么是循环队列
当ucTail=MAXSIZE-1
时,即已达到队列空间最末处时,再次入队时,ucTail=0
,重新指向队列空间的开始位置。
一般情况
队空
队满
如何判断队空队满
由上图的情况可以知道,ucHead与ucTail已经无法区分,故需要对现有标识物进行改造。
现在引入队列含有节点的个数icount
:
当队列为空时,icount=0
;
当队列为满时,icount=MAXSIZE
;
当入队时,icount++
;
当出队时,icount--
;
故循环队列的数据结构可以描述为:
#define MAXSIZE (128) //队列的最大长度
typedef struct _ring_queue_t_
{
unsigned char ucArray[MAXSIZE]; //所用的内存空间
unsigned char ucHead; //队头索引
unsigned char ucTail; //队尾索引
unsigned int iCount; //有效数据个数
}ring_queue_t;
其实,ucTail=ucHead+iCount
。但是会出现ucTail > MAXSIZE -1
的情况。故有ucTail=(ucHead+iCount)%MAXSIZE
为了减小数据结构占用的空间,可将上面的数据结构优化为:
#define MAXSIZE (128) //队列的最大长度
typedef struct _ring_queue_t_
{
unsigned char ucArray[MAXSIZE]; //所用的内存空间
unsigned char ucHead; //队头索引
unsigned int iCount; //有效数据个数
}ring_queue_t;
如何入队出队
入队
unsigned char ucTail = (ucHead + iCount) % MAXSIZE;
ucArray[ucTail] = x;
icount ++;
出队
ucHead++;
ucHead %= MAXSIZE
iCount --;
队列基本操作
1. 创建
2. 销毁
3. 入队
4. 出队
5. 清空队列
6. 判断队列为空
7. 判断队列为满
8. 获得队列长度
9. 读取队列某一位置的元素值
循环队列的接口与实现
https://gitee.com/hany_li/hany_gear_lib/tree/master/ring_buffer
队列的应用
- 解决主机与外设之间速度不匹配的问题,比如主机与打印机,可设置一个打印数据缓冲区。
- 解决由多用户引起的资源竞争问题,比如CPU资源的竞争,OS按照每个请求的先后顺序,排成一个队列,依次处理。
- 方便进行事件驱动设计。
事件驱动是一种简单而强大的程序设计方法,其思路是在事件发生时,将事件处理函数及参数保存到事件队列中。
而main()函数里则不断地查询事件队列中是否有未处理的事件,如果有,将其出队并调用其处理函数。
事件驱动程序设计的main()函数仅仅是不断地调用事件检测函数event_check()和事件队列处理函数do_event();
其参考代码如下:
int main(void)
{
queue_adt queue;
//创建一个事件队列
queue=new_event_queue();
for(;;)
{
//事件检测函数
event_check(queue);
//事件队列处理函数
do_event(queue);
}
//销毁指定事件队列
destroy_event_queue(queue);
return 0;
}
通过上面的分析可知,事件驱动程序设计的关键是设计事件队列处理代码。由于已经有了队列代码,因此可重用队列代码,简化事件队列设计。
创建和删除事件队列可以直接利用队列中创建和销毁函数。用#define重新定义一个名字即可。即:
#define event_queue_create() rb_create()
#define event_queue_destroy() rb_destroy()
事件队列处理代码最主要的部分是事件入队和处理入队的事件(即事件出队),其关键是用什么表示事件。
一般来说,一个事件必须有代码来处理它,否则这个事件对程序就没有意义。
而C语言中表示独立代码段的是函数,因此可以用指向事件处理函数的函数指针
及事件处理函数所需的参数
代表这个事件。
为了举例方便,事件处理函数统一定义为无返回值,只有一个参数的函数,如下:
struct queue_event
{
void (*event_fn)(void *p_arg);
void *p_arg;
}
那么入队函数接口如下:
void event_queue_in(rb_adt_t rb, void (*event)(void *p_arg), void *p_arg)
或
void event_queue_in(rb_adt_t rb, struct queue_event * event_instance)
那么什么时候调用事件入队呢?
就在检测事件event_check()中,若发现了新的事件,就将其入队即可。
优点:能依次处理多个不同的事件,如按键事件,时间事件等等,每个事件由具体的函数与参数来明确其行为。